Enhance Content Management with New Taxonomy and Attribute Models

- Introduced `ContentTaxonomy` and `ContentAttribute` models for improved content categorization and attribute management.
- Updated `Content` model to support new fields for content format, cluster role, and external type.
- Refactored serializers and views to accommodate new models, including `ContentTaxonomySerializer` and `ContentAttributeSerializer`.
- Added new API endpoints for managing taxonomies and attributes, enhancing the content management capabilities.
- Updated admin interfaces for `Content`, `ContentTaxonomy`, and `ContentAttribute` to reflect new structures and improve usability.
- Implemented backward compatibility for existing attribute mappings.
- Enhanced filtering and search capabilities in the API for better content retrieval.
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-22 00:21:00 +00:00
parent a82be89d21
commit 55dfd5ad19
17 changed files with 2934 additions and 40 deletions

View File

@@ -202,21 +202,65 @@ class Content(SiteSectorBaseModel):
# Phase 8: Universal Content Types
ENTITY_TYPE_CHOICES = [
('blog_post', 'Blog Post'),
('article', 'Article'),
('post', 'Blog Post'),
('page', 'Page'),
('product', 'Product'),
('service', 'Service Page'),
('taxonomy', 'Taxonomy Page'),
('page', 'Page'),
('taxonomy_term', 'Taxonomy Term Page'),
# Legacy choices for backward compatibility
('blog_post', 'Blog Post (Legacy)'),
('article', 'Article (Legacy)'),
('taxonomy', 'Taxonomy Page (Legacy)'),
]
entity_type = models.CharField(
max_length=50,
choices=ENTITY_TYPE_CHOICES,
default='blog_post',
default='post',
db_index=True,
help_text="Type of content entity"
)
# Phase 9: Content format (for posts)
CONTENT_FORMAT_CHOICES = [
('article', 'Article'),
('listicle', 'Listicle'),
('guide', 'How-To Guide'),
('comparison', 'Comparison'),
('review', 'Review'),
('roundup', 'Roundup'),
]
content_format = models.CharField(
max_length=50,
choices=CONTENT_FORMAT_CHOICES,
blank=True,
null=True,
db_index=True,
help_text="Content format (only for entity_type=post)"
)
# Phase 9: Cluster role
CLUSTER_ROLE_CHOICES = [
('hub', 'Hub Page'),
('supporting', 'Supporting Content'),
('attribute', 'Attribute Page'),
]
cluster_role = models.CharField(
max_length=50,
choices=CLUSTER_ROLE_CHOICES,
default='supporting',
blank=True,
null=True,
db_index=True,
help_text="Role within cluster strategy"
)
# Phase 9: WordPress post type
external_type = models.CharField(
max_length=100,
blank=True,
help_text="WordPress post type (post, page, product, service)"
)
# Phase 8: Structured content blocks
json_blocks = models.JSONField(
default=list,
@@ -231,6 +275,25 @@ class Content(SiteSectorBaseModel):
help_text="Content structure data (metadata, schema, etc.)"
)
# Phase 9: Taxonomy relationships
taxonomies = models.ManyToManyField(
'ContentTaxonomy',
blank=True,
related_name='contents',
through='ContentTaxonomyRelation',
help_text="Associated taxonomy terms (categories, tags, attributes)"
)
# Phase 9: Direct cluster relationship
cluster = models.ForeignKey(
'planner.Clusters',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='contents',
help_text="Primary semantic cluster"
)
class Meta:
app_label = 'writer'
db_table = 'igny8_content'
@@ -243,7 +306,12 @@ class Content(SiteSectorBaseModel):
models.Index(fields=['source']),
models.Index(fields=['sync_status']),
models.Index(fields=['source', 'sync_status']),
models.Index(fields=['entity_type']), # Phase 8
models.Index(fields=['entity_type']),
models.Index(fields=['content_format']),
models.Index(fields=['cluster_role']),
models.Index(fields=['cluster']),
models.Index(fields=['external_type']),
models.Index(fields=['site', 'entity_type']),
]
def save(self, *args, **kwargs):
@@ -261,6 +329,134 @@ class Content(SiteSectorBaseModel):
return f"Content for {self.task.title}"
class ContentTaxonomy(SiteSectorBaseModel):
"""
Universal taxonomy model for categories, tags, and product attributes.
Syncs with WordPress taxonomies and stores terms.
"""
TAXONOMY_TYPE_CHOICES = [
('category', 'Category'),
('tag', 'Tag'),
('product_cat', 'Product Category'),
('product_tag', 'Product Tag'),
('product_attr', 'Product Attribute'),
('service_cat', 'Service Category'),
]
SYNC_STATUS_CHOICES = [
('native', 'Native IGNY8'),
('imported', 'Imported from External'),
('synced', 'Synced with External'),
]
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"
)
description = models.TextField(blank=True, help_text="Term description")
parent = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.CASCADE,
related_name='children',
help_text="Parent term for hierarchical taxonomies"
)
# WordPress/WooCommerce sync fields
external_id = models.IntegerField(
null=True,
blank=True,
db_index=True,
help_text="WordPress term ID"
)
external_taxonomy = models.CharField(
max_length=100,
blank=True,
help_text="WP taxonomy name (category, post_tag, product_cat, pa_color)"
)
sync_status = models.CharField(
max_length=50,
choices=SYNC_STATUS_CHOICES,
default='native',
db_index=True,
help_text="Sync status with external system"
)
# WordPress metadata
count = models.IntegerField(default=0, help_text="Post/product count from WordPress")
metadata = models.JSONField(default=dict, blank=True, help_text="Additional metadata")
# Cluster mapping
clusters = models.ManyToManyField(
'planner.Clusters',
blank=True,
related_name='taxonomy_terms',
help_text="Semantic clusters this term maps to"
)
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=['sync_status']),
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 ContentTaxonomyRelation(models.Model):
"""
Through model for Content-Taxonomy M2M relationship.
Simplified without SiteSectorBaseModel to avoid tenant_id issues.
"""
content = models.ForeignKey(
Content,
on_delete=models.CASCADE,
related_name='taxonomy_relations'
)
taxonomy = models.ForeignKey(
ContentTaxonomy,
on_delete=models.CASCADE,
related_name='content_relations'
)
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']]
indexes = [
models.Index(fields=['content']),
models.Index(fields=['taxonomy']),
]
def __str__(self):
return f"{self.content}{self.taxonomy}"
class Images(SiteSectorBaseModel):
"""Images model for content-related images (featured, desktop, mobile, in-article)"""
@@ -447,19 +643,29 @@ class ContentTaxonomyMap(SiteSectorBaseModel):
return f"{self.taxonomy.name}"
class ContentAttributeMap(SiteSectorBaseModel):
"""Stores structured attribute data tied to content/task records."""
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='attribute_mappings',
related_name='attributes',
null=True,
blank=True,
)
@@ -470,20 +676,50 @@ class ContentAttributeMap(SiteSectorBaseModel):
null=True,
blank=True,
)
name = models.CharField(max_length=120)
value = models.CharField(max_length=255, blank=True, null=True)
source = models.CharField(max_length=50, choices=SOURCE_CHOICES, default='blueprint')
metadata = models.JSONField(default=dict, 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_attribute_map'
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=['task', 'name']),
models.Index(fields=['content', 'attribute_type']),
models.Index(fields=['cluster', 'attribute_type']),
models.Index(fields=['external_id']),
]
def save(self, *args, **kwargs):
@@ -495,5 +731,8 @@ class ContentAttributeMap(SiteSectorBaseModel):
super().save(*args, **kwargs)
def __str__(self):
target = self.content or self.task
return f"{target} {self.name}"
return f"{self.name}: {self.value}"
# Backward compatibility alias
ContentAttributeMap = ContentAttribute