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_TYPE_CHOICES = [ ('post', 'Post'), ('page', 'Page'), ('product', 'Product'), ('taxonomy', 'Taxonomy'), ] CONTENT_STRUCTURE_CHOICES = [ # Post structures ('article', 'Article'), ('guide', 'Guide'), ('comparison', 'Comparison'), ('review', 'Review'), ('listicle', 'Listicle'), # Page structures ('landing_page', 'Landing Page'), ('business_page', 'Business Page'), ('service_page', 'Service Page'), ('general', 'General'), ('cluster_hub', 'Cluster Hub'), # Product structures ('product_page', 'Product Page'), # Taxonomy structures ('category_archive', 'Category Archive'), ('tag_archive', 'Tag Archive'), ('attribute_archive', 'Attribute Archive'), ] title = models.CharField(max_length=255, db_index=True) description = models.TextField(blank=True, null=True) cluster = models.ForeignKey( 'planner.Clusters', on_delete=models.SET_NULL, null=True, blank=False, related_name='tasks', limit_choices_to={'sector': models.F('sector')}, help_text="Parent cluster (required)" ) idea = models.ForeignKey( 'planner.ContentIdeas', on_delete=models.SET_NULL, null=True, blank=True, related_name='tasks', help_text="Optional content idea reference", db_column='idea_id' ) content_type = models.CharField( max_length=100, db_index=True, help_text="Content type: post, page, product, taxonomy", choices=CONTENT_TYPE_CHOICES, default='post', blank=True, null=True ) content_structure = models.CharField( max_length=100, db_index=True, help_text="Content structure: article, guide, comparison, review, listicle, landing_page, etc.", choices=CONTENT_STRUCTURE_CHOICES, default='article', blank=True, null=True ) taxonomy_term = models.ForeignKey( 'ContentTaxonomy', on_delete=models.SET_NULL, null=True, blank=True, related_name='tasks', help_text="Optional taxonomy term assignment", db_column='taxonomy_id' ) keywords = models.TextField( blank=True, null=True, help_text="Comma-separated keywords for this task" ) status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='queued') 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=['content_structure']), models.Index(fields=['site', 'sector']), ] def __str__(self): return self.title class ContentTaxonomyRelation(models.Model): """Through model for Content-Taxonomy many-to-many relationship""" content = models.ForeignKey('Content', on_delete=models.CASCADE, db_column='content_id') taxonomy = models.ForeignKey('ContentTaxonomy', on_delete=models.CASCADE, db_column='taxonomy_id') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: app_label = 'writer' db_table = 'igny8_content_taxonomy_relations' unique_together = [['content', 'taxonomy']] class Content(SiteSectorBaseModel): """ Content model for AI-generated or WordPress-imported content. Final architecture: simplified content management. """ CONTENT_TYPE_CHOICES = [ ('post', 'Post'), ('page', 'Page'), ('product', 'Product'), ('taxonomy', 'Taxonomy'), ] CONTENT_STRUCTURE_CHOICES = [ # Post structures ('article', 'Article'), ('guide', 'Guide'), ('comparison', 'Comparison'), ('review', 'Review'), ('listicle', 'Listicle'), # Page structures ('landing_page', 'Landing Page'), ('business_page', 'Business Page'), ('service_page', 'Service Page'), ('general', 'General'), ('cluster_hub', 'Cluster Hub'), # Product structures ('product_page', 'Product Page'), # Taxonomy structures ('category_archive', 'Category Archive'), ('tag_archive', 'Tag Archive'), ('attribute_archive', 'Attribute Archive'), ] # Core content fields title = models.CharField(max_length=255, db_index=True) content_html = models.TextField(help_text="Final HTML content") cluster = models.ForeignKey( 'planner.Clusters', on_delete=models.SET_NULL, null=True, blank=False, related_name='contents', help_text="Parent cluster (required)" ) content_type = models.CharField( max_length=50, choices=CONTENT_TYPE_CHOICES, default='post', db_index=True, help_text="Content type: post, page, product, taxonomy" ) content_structure = models.CharField( max_length=50, choices=CONTENT_STRUCTURE_CHOICES, default='article', db_index=True, help_text="Content structure/format based on content type" ) # Taxonomy relationships taxonomy_terms = models.ManyToManyField( 'ContentTaxonomy', through='ContentTaxonomyRelation', blank=True, related_name='contents', help_text="Associated taxonomy terms (categories, tags, attributes)" ) # External platform fields (WordPress integration) external_id = models.CharField(max_length=255, blank=True, null=True, db_index=True, help_text="WordPress/external platform post ID") external_url = models.URLField(blank=True, null=True, help_text="WordPress/external platform URL") external_type = models.CharField(max_length=100, blank=True, null=True, help_text="WordPress post type (post, page, product, etc.)") sync_status = models.CharField(max_length=50, blank=True, null=True, help_text="Sync status with WordPress") # Source tracking SOURCE_CHOICES = [ ('igny8', 'IGNY8 Generated'), ('wordpress', 'WordPress Imported'), ] source = models.CharField( max_length=50, choices=SOURCE_CHOICES, default='igny8', db_index=True, help_text="Content source" ) # Status tracking STATUS_CHOICES = [ ('draft', 'Draft'), ('published', 'Published'), ] status = models.CharField( max_length=50, choices=STATUS_CHOICES, default='draft', db_index=True, help_text="Content status" ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: app_label = 'writer' db_table = 'igny8_content' ordering = ['-created_at'] verbose_name = 'Content' verbose_name_plural = 'Contents' indexes = [ models.Index(fields=['title']), models.Index(fields=['cluster']), models.Index(fields=['content_type']), models.Index(fields=['content_structure']), models.Index(fields=['source']), models.Index(fields=['status']), models.Index(fields=['external_id']), models.Index(fields=['site', 'sector']), ] def __str__(self): return self.title or f"Content {self.id}" class ContentTaxonomy(SiteSectorBaseModel): """ Universal taxonomy model for WordPress and IGNY8 cluster-based taxonomies. Supports categories, tags, product attributes, and cluster mappings. """ TAXONOMY_TYPE_CHOICES = [ ('category', 'Category'), ('tag', 'Tag'), ('product_category', 'Product Category'), ('product_attribute', 'Product Attribute'), ('cluster', 'Cluster Taxonomy'), ] name = models.CharField(max_length=255, db_index=True, help_text="Term name") slug = models.SlugField(max_length=255, db_index=True, help_text="URL slug") taxonomy_type = models.CharField( max_length=50, choices=TAXONOMY_TYPE_CHOICES, db_index=True, help_text="Type of taxonomy" ) # WordPress/external platform sync fields external_taxonomy = models.CharField( max_length=100, blank=True, null=True, help_text="WordPress taxonomy slug (category, post_tag, product_cat, pa_*) - null for cluster taxonomies" ) external_id = models.IntegerField( null=True, blank=True, db_index=True, help_text="WordPress term_id - null for cluster taxonomies" ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: app_label = 'writer' db_table = 'igny8_content_taxonomy_terms' verbose_name = 'Content Taxonomy' verbose_name_plural = 'Content Taxonomies' unique_together = [ ['site', 'slug', 'taxonomy_type'], ['site', 'external_id', 'external_taxonomy'], ] indexes = [ models.Index(fields=['name']), models.Index(fields=['slug']), models.Index(fields=['taxonomy_type']), models.Index(fields=['external_id', 'external_taxonomy']), models.Index(fields=['site', 'taxonomy_type']), models.Index(fields=['site', 'sector']), ] def __str__(self): return f"{self.name} ({self.get_taxonomy_type_display()})" 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}" class ContentClusterMap(SiteSectorBaseModel): """Associates generated content with planner clusters + roles.""" ROLE_CHOICES = [ ('hub', 'Hub Page'), ('supporting', 'Supporting Page'), ('attribute', 'Attribute Page'), ] SOURCE_CHOICES = [ ('blueprint', 'Blueprint'), ('manual', 'Manual'), ('import', 'Import'), ] content = models.ForeignKey( Content, on_delete=models.CASCADE, related_name='cluster_mappings', null=True, blank=True, ) task = models.ForeignKey( Tasks, on_delete=models.CASCADE, related_name='cluster_mappings', null=True, blank=True, ) cluster = models.ForeignKey( 'planner.Clusters', on_delete=models.CASCADE, related_name='content_mappings', ) role = models.CharField(max_length=50, choices=ROLE_CHOICES, default='hub') source = models.CharField(max_length=50, choices=SOURCE_CHOICES, default='blueprint') metadata = models.JSONField(default=dict, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: app_label = 'writer' db_table = 'igny8_content_cluster_map' unique_together = [['content', 'cluster', 'role']] indexes = [ models.Index(fields=['cluster', 'role']), models.Index(fields=['content', 'role']), models.Index(fields=['task', 'role']), ] def save(self, *args, **kwargs): provider = self.content or self.task if provider: self.account = provider.account self.site = provider.site self.sector = provider.sector super().save(*args, **kwargs) def __str__(self): return f"{self.cluster.name} ({self.get_role_display()})" class ContentTaxonomyMap(SiteSectorBaseModel): """Maps content entities to blueprint taxonomies for syncing/publishing.""" SOURCE_CHOICES = [ ('blueprint', 'Blueprint'), ('manual', 'Manual'), ('import', 'Import'), ] content = models.ForeignKey( Content, on_delete=models.CASCADE, related_name='taxonomy_mappings', null=True, blank=True, ) task = models.ForeignKey( Tasks, on_delete=models.CASCADE, related_name='taxonomy_mappings', null=True, blank=True, ) taxonomy = models.ForeignKey( 'site_building.SiteBlueprintTaxonomy', on_delete=models.CASCADE, related_name='content_mappings', ) source = models.CharField(max_length=50, choices=SOURCE_CHOICES, default='blueprint') metadata = models.JSONField(default=dict, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: app_label = 'writer' db_table = 'igny8_content_taxonomy_map' unique_together = [['content', 'taxonomy']] indexes = [ models.Index(fields=['taxonomy']), models.Index(fields=['content', 'taxonomy']), models.Index(fields=['task', 'taxonomy']), ] def save(self, *args, **kwargs): provider = self.content or self.task if provider: self.account = provider.account self.site = provider.site self.sector = provider.sector super().save(*args, **kwargs) def __str__(self): return f"{self.taxonomy.name}" class ContentAttribute(SiteSectorBaseModel): """ Unified attribute storage for products, services, and semantic facets. Replaces ContentAttributeMap with enhanced WP sync support. """ ATTRIBUTE_TYPE_CHOICES = [ ('product_spec', 'Product Specification'), ('service_modifier', 'Service Modifier'), ('semantic_facet', 'Semantic Facet'), ] SOURCE_CHOICES = [ ('blueprint', 'Blueprint'), ('manual', 'Manual'), ('import', 'Import'), ('wordpress', 'WordPress'), ] content = models.ForeignKey( Content, on_delete=models.CASCADE, related_name='attributes', null=True, blank=True, ) task = models.ForeignKey( Tasks, on_delete=models.CASCADE, related_name='attribute_mappings', null=True, blank=True, ) cluster = models.ForeignKey( 'planner.Clusters', on_delete=models.SET_NULL, null=True, blank=True, related_name='attributes', help_text="Optional cluster association for semantic attributes" ) attribute_type = models.CharField( max_length=50, choices=ATTRIBUTE_TYPE_CHOICES, default='product_spec', db_index=True, help_text="Type of attribute" ) name = models.CharField(max_length=120, help_text="Attribute name (e.g., Color, Material)") value = models.CharField(max_length=255, blank=True, null=True, help_text="Attribute value (e.g., Blue, Cotton)") # WordPress/WooCommerce sync fields external_id = models.IntegerField(null=True, blank=True, help_text="WP attribute term ID") external_attribute_name = models.CharField( max_length=100, blank=True, help_text="WP attribute slug (e.g., pa_color, pa_size)" ) source = models.CharField(max_length=50, choices=SOURCE_CHOICES, default='manual') metadata = models.JSONField(default=dict, blank=True, help_text="Additional metadata") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: app_label = 'writer' db_table = 'igny8_content_attributes' verbose_name = 'Content Attribute' verbose_name_plural = 'Content Attributes' indexes = [ models.Index(fields=['name']), models.Index(fields=['attribute_type']), models.Index(fields=['content', 'name']), models.Index(fields=['content', 'attribute_type']), models.Index(fields=['cluster', 'attribute_type']), models.Index(fields=['external_id']), ] def save(self, *args, **kwargs): provider = self.content or self.task if provider: self.account = provider.account self.site = provider.site self.sector = provider.sector super().save(*args, **kwargs) def __str__(self): return f"{self.name}: {self.value}" # Backward compatibility alias ContentAttributeMap = ContentAttribute