be fe fixes

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-26 10:43:51 +00:00
parent 4fe68cc271
commit 1cbc347cdc
15 changed files with 5557 additions and 149 deletions

View File

@@ -22,15 +22,30 @@ class Tasks(SiteSectorBaseModel):
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, service, category, tag, etc."
help_text="Content type: post, page, product, service, category, tag, etc.",
db_column='entity_type',
blank=True,
null=True
)
content_structure = models.CharField(
max_length=100,
db_index=True,
help_text="Content structure/format: article, listicle, guide, comparison, product_page, etc."
help_text="Content structure/format: article, listicle, guide, comparison, product_page, etc.",
db_column='cluster_role',
blank=True,
null=True
)
taxonomy_term = models.ForeignKey(
'ContentTaxonomy',
@@ -38,13 +53,13 @@ class Tasks(SiteSectorBaseModel):
null=True,
blank=True,
related_name='tasks',
help_text="Optional taxonomy term assignment"
help_text="Optional taxonomy term assignment",
db_column='taxonomy_id'
)
keywords = models.ManyToManyField(
'planner.Keywords',
keywords = models.TextField(
blank=True,
related_name='tasks',
help_text="Keywords linked to this task"
null=True,
help_text="Comma-separated keywords for this task"
)
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='queued')
@@ -70,6 +85,19 @@ class Tasks(SiteSectorBaseModel):
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.
@@ -78,7 +106,7 @@ class Content(SiteSectorBaseModel):
# Core content fields
title = models.CharField(max_length=255, db_index=True)
content_html = models.TextField(help_text="Final HTML content")
content_html = models.TextField(help_text="Final HTML content", db_column='html_content')
cluster = models.ForeignKey(
'planner.Clusters',
on_delete=models.SET_NULL,
@@ -90,26 +118,34 @@ class Content(SiteSectorBaseModel):
content_type = models.CharField(
max_length=100,
db_index=True,
help_text="Content type: post, page, product, service, category, tag, etc."
help_text="Content type: post, page, product, service, category, tag, etc.",
db_column='entity_type',
blank=True,
null=True
)
content_structure = models.CharField(
max_length=100,
db_index=True,
help_text="Content structure/format: article, listicle, guide, comparison, product_page, etc."
help_text="Content structure/format: article, listicle, guide, comparison, product_page, etc.",
db_column='cluster_role',
blank=True,
null=True
)
# Taxonomy relationships
taxonomy_terms = models.ManyToManyField(
'ContentTaxonomy',
through='ContentTaxonomyRelation',
blank=True,
related_name='contents',
db_table='igny8_content_taxonomy_relations',
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 = [

View File

@@ -23,62 +23,21 @@ class MetadataMappingService:
@transaction.atomic
def persist_task_metadata_to_content(self, content: Content) -> None:
"""
Persist cluster/taxonomy/attribute mappings from Task to Content.
DEPRECATED: This method is deprecated as Content model no longer has task field.
Metadata is now persisted directly on content model.
Args:
content: Content instance with an associated task
content: Content instance
"""
if not content.task:
logger.warning(f"Content {content.id} has no associated task, skipping metadata mapping")
return
task = content.task
# Stage 3: Persist cluster mapping if task has cluster
if task.cluster:
ContentClusterMap.objects.get_or_create(
content=content,
cluster=task.cluster,
role=task.cluster_role or 'hub',
defaults={
'account': content.account,
'site': content.site,
'sector': content.sector,
'source': 'blueprint' if task.idea else 'manual',
'metadata': {},
}
)
logger.info(f"Created cluster mapping for content {content.id} -> cluster {task.cluster.id}")
# Stage 3: Persist taxonomy mapping if task has taxonomy
if task.taxonomy:
ContentTaxonomyMap.objects.get_or_create(
content=content,
taxonomy=task.taxonomy,
defaults={
'account': content.account,
'site': content.site,
'sector': content.sector,
'source': 'blueprint',
'metadata': {},
}
)
logger.info(f"Created taxonomy mapping for content {content.id} -> taxonomy {task.taxonomy.id}")
# Stage 3: Inherit entity_type from task
if task.entity_type and not content.entity_type:
content.entity_type = task.entity_type
content.save(update_fields=['entity_type'])
logger.info(f"Set entity_type {task.entity_type} for content {content.id}")
# Stage 3: Extract attributes from task metadata if available
# This can be extended to parse task.description or task.metadata for attributes
# For now, we'll rely on explicit attribute data in future enhancements
logger.warning(f"[persist_task_metadata_to_content] Deprecated method called for content {content.id}")
logger.warning(f"Content model no longer has task field - metadata should be set directly on content")
return
@transaction.atomic
def backfill_content_metadata(self, content: Content) -> None:
"""
Backfill metadata mappings for existing content that may be missing mappings.
Note: task field was removed from Content model - only infers from content metadata.
Args:
content: Content instance to backfill
@@ -87,13 +46,24 @@ class MetadataMappingService:
if ContentClusterMap.objects.filter(content=content).exists():
return
# Try to infer from task
if content.task:
self.persist_task_metadata_to_content(content)
# Try to infer from content metadata or cluster field
if hasattr(content, 'cluster') and content.cluster:
ContentClusterMap.objects.get_or_create(
content=content,
cluster=content.cluster,
role='hub', # Default
defaults={
'account': content.account,
'site': content.site,
'sector': content.sector,
'source': 'manual',
'metadata': {},
}
)
return
# Try to infer from content metadata
if content.metadata:
# Fallback: Try to infer from content metadata
if hasattr(content, 'metadata') and content.metadata:
cluster_id = content.metadata.get('cluster_id')
if cluster_id:
from igny8_core.business.planning.models import Clusters

View File

@@ -37,20 +37,20 @@ class ContentValidationService:
})
# Stage 3: Validate entity_type is set
if not task.entity_type:
if not task.content_type:
errors.append({
'field': 'entity_type',
'code': 'missing_entity_type',
'message': 'Task must have an entity type specified',
'field': 'content_type',
'code': 'missing_content_type',
'message': 'Task must have a content type specified',
})
# Stage 3: Validate taxonomy for product/service entities
if task.entity_type in ['product', 'service']:
if not task.taxonomy:
if task.content_type in ['product', 'service']:
if not task.taxonomy_term:
errors.append({
'field': 'taxonomy',
'code': 'missing_taxonomy',
'message': f'{task.entity_type.title()} tasks require a taxonomy association',
'message': f'{task.content_type.title()} tasks require a taxonomy association',
})
return errors
@@ -67,12 +67,12 @@ class ContentValidationService:
"""
errors = []
# Stage 3: Validate entity_type
if not content.entity_type:
# Stage 3: Validate content_type
if not content.content_type:
errors.append({
'field': 'entity_type',
'code': 'missing_entity_type',
'message': 'Content must have an entity type specified',
'field': 'content_type',
'code': 'missing_content_type',
'message': 'Content must have a content type specified',
})
# Stage 3: Validate cluster mapping exists for IGNY8 content
@@ -86,30 +86,20 @@ class ContentValidationService:
})
# Stage 3: Validate taxonomy for product/service content
if content.entity_type in ['product', 'service']:
from igny8_core.business.content.models import ContentTaxonomyMap
if not ContentTaxonomyMap.objects.filter(content=content).exists():
if content.content_type in ['product', 'service']:
# Check if content has taxonomy terms assigned
if not content.taxonomy_terms.exists():
errors.append({
'field': 'taxonomy_mapping',
'code': 'missing_taxonomy_mapping',
'message': f'{content.entity_type.title()} content requires a taxonomy mapping',
'message': f'{content.content_type.title()} content requires a taxonomy mapping',
})
# Stage 3: Validate required attributes for products
if content.entity_type == 'product':
from igny8_core.business.content.models import ContentAttributeMap
required_attrs = ['price', 'sku', 'category']
existing_attrs = ContentAttributeMap.objects.filter(
content=content,
name__in=required_attrs
).values_list('name', flat=True)
missing_attrs = set(required_attrs) - set(existing_attrs)
if missing_attrs:
errors.append({
'field': 'attributes',
'code': 'missing_attributes',
'message': f'Product content requires attributes: {", ".join(missing_attrs)}',
})
if content.content_type == 'product':
# Product validation - currently simplified in Stage 1
# TODO: Re-enable attribute validation when product attributes are implemented
pass
return errors
@@ -136,9 +126,9 @@ class ContentValidationService:
'message': 'Content must have a title before publishing',
})
if not content.html_content or len(content.html_content.strip()) < 100:
if not content.content_html or len(content.content_html.strip()) < 100:
errors.append({
'field': 'html_content',
'field': 'content_html',
'code': 'insufficient_content',
'message': 'Content must have at least 100 characters before publishing',
})
@@ -157,9 +147,9 @@ class ContentValidationService:
"""
errors = []
if task.entity_type == 'product':
if task.content_type == 'product':
# Products should have taxonomy and cluster
if not task.taxonomy:
if not task.taxonomy_term:
errors.append({
'field': 'taxonomy',
'code': 'missing_taxonomy',