# -*- coding: utf-8 -*- """ itsulu.blog.generate.wizard — On-demand blog generation wizard. Used by: - Backend "Generate Now" button (in menu and on schedule records) - Website toolbar "Generate New Post" button (via JSON-RPC → wizard → redirect) After action_generate() completes, the user is sent to: - The blog post form view (backend) - The published post URL (website) """ import logging from odoo import api, fields, models from odoo.exceptions import UserError _logger = logging.getLogger(__name__) class BlogGenerateWizard(models.TransientModel): _name = 'itsulu.blog.generate.wizard' _description = 'Blog Publisher — Generate Now Wizard' # ------------------------------------------------------------------ # # Fields # # ------------------------------------------------------------------ # topic = fields.Char( string='Topic', help='Leave blank to use the next topic from the queue, ' 'or let the LLM choose based on ITSulu services.', ) blog_id = fields.Many2one( comodel_name='blog.blog', string='Target Blog', required=True, ) llm_provider = fields.Selection( selection=[ ('anthropic', 'Anthropic Claude'), ('openai', 'OpenAI ChatGPT'), ('gemini', 'Google Gemini'), ('ollama', 'Ollama / Open WebUI'), ], string='Text LLM Provider', required=True, default='anthropic', ) llm_model = fields.Char( string='Text LLM Model', required=True, default='claude-sonnet-4-20250514', ) image_provider = fields.Selection( selection=[ ('none', 'No Image'), ('openai_dalle', 'OpenAI DALL·E 3'), ('google_imagen', 'Google Imagen'), ('stable_diff', 'Stable Diffusion (Ollama)'), ], string='Image Provider', default='none', required=True, ) auto_publish = fields.Boolean( string='Publish Immediately', default=True, ) tone = fields.Char( string='Tone', default='professional and informative', ) # Social platform toggles platform_twitter = fields.Boolean(string='X/Twitter', default=True) platform_bluesky = fields.Boolean(string='BlueSky', default=True) platform_mastodon = fields.Boolean(string='Mastodon', default=True) platform_linkedin = fields.Boolean(string='LinkedIn', default=True) retry_log_id = fields.Integer( string='Retry Log ID', help='Set when opening wizard from a failed log retry action.', ) # ------------------------------------------------------------------ # # Defaults # # ------------------------------------------------------------------ # @api.model def default_get(self, fields_list): res = super().default_get(fields_list) params = self.env['ir.config_parameter'].sudo() # Pull defaults from Settings if 'llm_provider' in fields_list: res['llm_provider'] = params.get_param( 'itsulu_blog_publisher.default_provider', 'anthropic' ) if 'llm_model' in fields_list: res['llm_model'] = params.get_param( 'itsulu_blog_publisher.default_model', 'claude-sonnet-4-20250514' ) if 'image_provider' in fields_list: res['image_provider'] = params.get_param( 'itsulu_blog_publisher.default_image_provider', 'none' ) if 'blog_id' in fields_list: blog_id_str = params.get_param('itsulu_blog_publisher.default_blog_id', '') if blog_id_str and blog_id_str.isdigit(): res['blog_id'] = int(blog_id_str) else: # Fall back to first available blog first_blog = self.env['blog.blog'].search([], limit=1) if first_blog: res['blog_id'] = first_blog.id return res # ------------------------------------------------------------------ # # Action # # ------------------------------------------------------------------ # def action_generate(self): """ Validate inputs, create a temporary schedule record (or reuse an existing one matching this configuration), and delegate to BlogSchedule.run_generation(). Returns an ir.actions.act_window pointing to the created blog.post. """ self.ensure_one() if not self.blog_id: raise UserError("Please select a Target Blog before generating.") platform_overrides = { 'twitter_a': self.platform_twitter, 'twitter_b': self.platform_twitter, 'bluesky_a': self.platform_bluesky, 'bluesky_b': self.platform_bluesky, 'mastodon': self.platform_mastodon, 'linkedin': self.platform_linkedin, } # Build an ephemeral schedule record for this one-off generation # (We reuse BlogSchedule.run_generation to keep all orchestration in one place) ephemeral_slot = self.env['itsulu.blog.schedule'].create({ 'name': f'On-Demand: {self.topic or "Queue/LLM"}', 'slot': 'morning', # slot value is irrelevant for manual runs 'active': True, 'blog_id': self.blog_id.id, 'llm_provider': self.llm_provider, 'llm_model': self.llm_model, 'image_provider': self.image_provider, 'auto_publish': self.auto_publish, 'tone': self.tone or 'professional and informative', 'platform_twitter': self.platform_twitter, 'platform_bluesky': self.platform_bluesky, 'platform_mastodon': self.platform_mastodon, 'platform_linkedin': self.platform_linkedin, }) try: blog_post = ephemeral_slot.run_generation( topic=self.topic or None, trigger_source='manual', triggered_by=self.env.user, auto_publish=self.auto_publish, platform_overrides=platform_overrides, ) finally: # Clean up the ephemeral slot — we don't want orphaned records ephemeral_slot.unlink() if not blog_post: raise UserError( "Blog generation failed. Check the Generation Log for details." ) # Return action to open the created post return { 'type': 'ir.actions.act_window', 'name': 'Generated Blog Post', 'res_model': 'blog.post', 'res_id': blog_post.id, 'view_mode': 'form', 'target': 'current', } def action_generate_and_open_website(self): """ Generate and redirect to the public website URL. Called from the website toolbar wizard. """ result = self.action_generate() # Switch from backend form to website URL blog_post = self.env['blog.post'].browse(result.get('res_id')) if blog_post and blog_post.website_url: return { 'type': 'ir.actions.act_url', 'url': blog_post.website_url, 'target': 'self', } return result