from django.db import models from django.core.validators import MinValueValidator from igny8_core.auth.models import SiteSectorBaseModel class Tasks(SiteSectorBaseModel): """Tasks model for content generation queue""" STATUS_CHOICES = [ ('queued', 'Queued'), ('completed', 'Completed'), ] CONTENT_STRUCTURE_CHOICES = [ ('cluster_hub', 'Cluster Hub'), ('landing_page', 'Landing Page'), ('pillar_page', 'Pillar Page'), ('supporting_page', 'Supporting Page'), ] CONTENT_TYPE_CHOICES = [ ('blog_post', 'Blog Post'), ('article', 'Article'), ('guide', 'Guide'), ('tutorial', 'Tutorial'), ] title = models.CharField(max_length=255, db_index=True) description = models.TextField(blank=True, null=True) keywords = models.CharField(max_length=500, blank=True) # Comma-separated keywords (legacy) cluster = models.ForeignKey( 'planner.Clusters', on_delete=models.SET_NULL, null=True, blank=True, related_name='tasks', limit_choices_to={'sector': models.F('sector')} ) keyword_objects = models.ManyToManyField( 'planner.Keywords', blank=True, related_name='tasks', help_text="Individual keywords linked to this task" ) idea = models.ForeignKey( 'planner.ContentIdeas', on_delete=models.SET_NULL, null=True, blank=True, related_name='tasks' ) content_structure = models.CharField(max_length=50, choices=CONTENT_STRUCTURE_CHOICES, default='blog_post') content_type = models.CharField(max_length=50, choices=CONTENT_TYPE_CHOICES, default='blog_post') status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='queued') # Content fields content = models.TextField(blank=True, null=True) # Generated content word_count = models.IntegerField(default=0) # SEO fields meta_title = models.CharField(max_length=255, blank=True, null=True) meta_description = models.TextField(blank=True, null=True) # WordPress integration assigned_post_id = models.IntegerField(null=True, blank=True) # WordPress post ID if published post_url = models.URLField(blank=True, null=True) # WordPress post URL created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: app_label = 'writer' db_table = 'igny8_tasks' ordering = ['-created_at'] verbose_name = 'Task' verbose_name_plural = 'Tasks' indexes = [ models.Index(fields=['title']), models.Index(fields=['status']), models.Index(fields=['cluster']), models.Index(fields=['content_type']), models.Index(fields=['site', 'sector']), ] def __str__(self): return self.title class Content(SiteSectorBaseModel): """ Content model for storing final AI-generated article content. Separated from Task for content versioning and storage optimization. """ task = models.OneToOneField( Tasks, on_delete=models.CASCADE, related_name='content_record', help_text="The task this content belongs to" ) html_content = models.TextField(help_text="Final AI-generated HTML content") word_count = models.IntegerField(default=0, validators=[MinValueValidator(0)]) metadata = models.JSONField(default=dict, help_text="Additional metadata (SEO, structure, etc.)") title = models.CharField(max_length=255, blank=True, null=True) meta_title = models.CharField(max_length=255, blank=True, null=True) meta_description = models.TextField(blank=True, null=True) primary_keyword = models.CharField(max_length=255, blank=True, null=True) secondary_keywords = models.JSONField(default=list, blank=True, help_text="List of secondary keywords") tags = models.JSONField(default=list, blank=True, help_text="List of tags") categories = models.JSONField(default=list, blank=True, help_text="List of categories") STATUS_CHOICES = [ ('draft', 'Draft'), ('review', 'Review'), ('publish', 'Publish'), ] status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='draft', help_text="Content workflow status (draft, review, publish)") generated_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) # Phase 4: Source tracking SOURCE_CHOICES = [ ('igny8', 'IGNY8 Generated'), ('wordpress', 'WordPress Synced'), ('shopify', 'Shopify Synced'), ('custom', 'Custom API Synced'), ] source = models.CharField( max_length=50, choices=SOURCE_CHOICES, default='igny8', db_index=True, help_text="Source of the content" ) SYNC_STATUS_CHOICES = [ ('native', 'Native IGNY8 Content'), ('imported', 'Imported from External'), ('synced', 'Synced from External'), ] sync_status = models.CharField( max_length=50, choices=SYNC_STATUS_CHOICES, default='native', db_index=True, help_text="Sync status of the content" ) # External reference fields external_id = models.CharField(max_length=255, blank=True, null=True, help_text="External platform ID") external_url = models.URLField(blank=True, null=True, help_text="External platform URL") sync_metadata = models.JSONField(default=dict, blank=True, help_text="Platform-specific sync metadata") # Phase 4: Linking fields internal_links = models.JSONField(default=list, blank=True, help_text="Internal links added by linker") linker_version = models.IntegerField(default=0, help_text="Version of linker processing") # Phase 4: Optimization fields optimizer_version = models.IntegerField(default=0, help_text="Version of optimizer processing") optimization_scores = models.JSONField(default=dict, blank=True, help_text="Optimization scores (SEO, readability, engagement)") class Meta: app_label = 'writer' db_table = 'igny8_content' ordering = ['-generated_at'] verbose_name = 'Content' verbose_name_plural = 'Contents' indexes = [ models.Index(fields=['task']), models.Index(fields=['generated_at']), models.Index(fields=['source']), models.Index(fields=['sync_status']), models.Index(fields=['source', 'sync_status']), ] def save(self, *args, **kwargs): """Automatically set account, site, and sector from task""" if self.task: self.account = self.task.account self.site = self.task.site self.sector = self.task.sector super().save(*args, **kwargs) def __str__(self): return f"Content for {self.task.title}" class Images(SiteSectorBaseModel): """Images model for content-related images (featured, desktop, mobile, in-article)""" IMAGE_TYPE_CHOICES = [ ('featured', 'Featured Image'), ('desktop', 'Desktop Image'), ('mobile', 'Mobile Image'), ('in_article', 'In-Article Image'), ] content = models.ForeignKey( Content, on_delete=models.CASCADE, related_name='images', null=True, blank=True, help_text="The content this image belongs to (preferred)" ) task = models.ForeignKey( Tasks, on_delete=models.CASCADE, related_name='images', null=True, blank=True, help_text="The task this image belongs to (legacy, use content instead)" ) image_type = models.CharField(max_length=50, choices=IMAGE_TYPE_CHOICES, default='featured') image_url = models.CharField(max_length=500, blank=True, null=True, help_text="URL of the generated/stored image") image_path = models.CharField(max_length=500, blank=True, null=True, help_text="Local path if stored locally") prompt = models.TextField(blank=True, null=True, help_text="Image generation prompt used") status = models.CharField(max_length=50, default='pending', help_text="Status: pending, generated, failed") position = models.IntegerField(default=0, help_text="Position for in-article images ordering") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: app_label = 'writer' db_table = 'igny8_images' ordering = ['content', 'position', '-created_at'] verbose_name = 'Image' verbose_name_plural = 'Images' indexes = [ models.Index(fields=['content', 'image_type']), models.Index(fields=['task', 'image_type']), models.Index(fields=['status']), models.Index(fields=['content', 'position']), models.Index(fields=['task', 'position']), ] def save(self, *args, **kwargs): """Automatically set account, site, and sector from content or task""" # Prefer content over task if self.content: self.account = self.content.account self.site = self.content.site self.sector = self.content.sector elif self.task: self.account = self.task.account self.site = self.task.site self.sector = self.task.sector super().save(*args, **kwargs) def __str__(self): content_title = self.content.title if self.content else None task_title = self.task.title if self.task else None title = content_title or task_title or 'Unknown' return f"{title} - {self.image_type}"