# -*- coding: utf-8 -*- """ itsulu.blog.post.social — Social media copy for one published blog post. One record per blog.post. Stores the AI-generated platform copy so operators can review and edit before the notification email is sent, and for future reference. """ from odoo import api, fields, models from odoo.exceptions import ValidationError class BlogPostSocial(models.Model): _name = 'itsulu.blog.post.social' _description = 'Blog Publisher — Social Media Copy' _rec_name = 'blog_post_id' blog_post_id = fields.Many2one( comodel_name='blog.post', string='Blog Post', required=True, ondelete='cascade', index=True, ) # ------------------------------------------------------------------ # # X / Twitter # # ------------------------------------------------------------------ # twitter_post_a = fields.Text( string='X / Twitter — Post A', help='Max 280 characters excluding URL. Hook: statistic or question.', ) twitter_post_b = fields.Text( string='X / Twitter — Post B', help='Max 280 characters excluding URL. Different angle from Post A.', ) twitter_enabled = fields.Boolean(string='Generate X/Twitter', default=True) # ------------------------------------------------------------------ # # BlueSky # # ------------------------------------------------------------------ # bluesky_post_a = fields.Text( string='BlueSky — Post A', help='Max 300 characters excluding URL.', ) bluesky_post_b = fields.Text( string='BlueSky — Post B', help='Max 300 characters excluding URL.', ) bluesky_enabled = fields.Boolean(string='Generate BlueSky', default=True) # ------------------------------------------------------------------ # # Fediverse / Mastodon # # ------------------------------------------------------------------ # mastodon_post = fields.Text( string='Fediverse / Mastodon', help='Max 500 characters excluding URL. Community-oriented tone.', ) mastodon_enabled = fields.Boolean(string='Generate Mastodon', default=True) # ------------------------------------------------------------------ # # LinkedIn # # ------------------------------------------------------------------ # linkedin_post = fields.Text( string='LinkedIn', help='150–3000 characters. Professional, data-driven. Include insight + CTA.', ) linkedin_enabled = fields.Boolean(string='Generate LinkedIn', default=True) # ------------------------------------------------------------------ # # Sources # # ------------------------------------------------------------------ # sources_referenced = fields.Text( string='Sources Referenced', help='Newline-separated list of "Title — URL" pairs cited by the LLM.', ) # ------------------------------------------------------------------ # # Constraints # # ------------------------------------------------------------------ # _sql_constraints = [ ( 'blog_post_unique', 'UNIQUE(blog_post_id)', 'Each blog post can have only one social media copy record.', ) ] @api.constrains('twitter_post_a', 'twitter_post_b') def _check_twitter_length(self): """Warn (not error) when Twitter posts exceed 280 characters.""" for rec in self: for field_name in ('twitter_post_a', 'twitter_post_b'): value = getattr(rec, field_name) or '' # Strip {{URL}} placeholder before counting stripped = value.replace('{{URL}}', '').strip() if len(stripped) > 280: raise ValidationError( f"X/Twitter post ({field_name}) is {len(stripped)} characters " f"(max 280 excluding URL). Please shorten it." ) @api.constrains('mastodon_post') def _check_mastodon_length(self): for rec in self: value = (rec.mastodon_post or '').replace('{{URL}}', '').strip() if len(value) > 500: raise ValidationError( f"Mastodon post is {len(value)} characters (max 500 excluding URL)." ) # ------------------------------------------------------------------ # # Helper # # ------------------------------------------------------------------ # def substitute_url(self, post_url: str) -> 'BlogPostSocial': """ Replace {{URL}} placeholder in all social copy fields with the actual URL. Called just before sending the notification email. Returns self for chaining. """ self.ensure_one() fields_to_update = [ 'twitter_post_a', 'twitter_post_b', 'bluesky_post_a', 'bluesky_post_b', 'mastodon_post', 'linkedin_post', ] vals = {} for fname in fields_to_update: current = getattr(self, fname) or '' if '{{URL}}' in current: vals[fname] = current.replace('{{URL}}', post_url) if vals: self.write(vals) return self