refactor-migration again
This commit is contained in:
@@ -208,29 +208,16 @@ class GenerateIdeasFunction(BaseAIFunction):
|
||||
# Handle target_keywords
|
||||
target_keywords = idea_data.get('covered_keywords', '') or idea_data.get('target_keywords', '')
|
||||
|
||||
# Map content_type and content_structure to ContentIdeas fields
|
||||
# AI returns: content_type (post/page/product/service) → site_entity_type
|
||||
# AI returns: content_structure (article/guide/review/comparison) → cluster_role (hub/supporting/attribute)
|
||||
site_entity_type = idea_data.get('content_type', 'post') # post, page, product, service
|
||||
|
||||
# Map content_structure to cluster_role
|
||||
# Direct mapping - no conversion needed
|
||||
content_type = idea_data.get('content_type', 'post')
|
||||
content_structure = idea_data.get('content_structure', 'article')
|
||||
structure_to_role = {
|
||||
'article': 'hub',
|
||||
'guide': 'supporting',
|
||||
'review': 'supporting',
|
||||
'comparison': 'attribute',
|
||||
'listicle': 'supporting',
|
||||
'product_page': 'attribute',
|
||||
}
|
||||
cluster_role = structure_to_role.get(content_structure, 'hub')
|
||||
|
||||
# Create ContentIdeas record
|
||||
ContentIdeas.objects.create(
|
||||
idea_title=idea_data.get('title', 'Untitled Idea'),
|
||||
description=description, # Stored as JSON string
|
||||
site_entity_type=site_entity_type,
|
||||
cluster_role=cluster_role,
|
||||
content_type=content_type,
|
||||
content_structure=content_structure,
|
||||
target_keywords=target_keywords,
|
||||
keyword_cluster=cluster,
|
||||
estimated_word_count=idea_data.get('estimated_word_count', 1500),
|
||||
|
||||
@@ -145,7 +145,15 @@ Output JSON Example:
|
||||
"covered_keywords": "organic duvet covers, eco-friendly bedding, sustainable sheets"
|
||||
}
|
||||
]
|
||||
}""",
|
||||
}
|
||||
|
||||
Valid content_type values: post, page, product, taxonomy
|
||||
|
||||
Valid content_structure by type:
|
||||
- post: article, guide, comparison, review, listicle
|
||||
- page: landing_page, business_page, service_page, general, cluster_hub
|
||||
- product: product_page
|
||||
- taxonomy: category_archive, tag_archive, attribute_archive""",
|
||||
|
||||
'content_generation': """You are an editorial content strategist. Your task is to generate a complete JSON response object based on the provided content idea, keyword cluster, keyword list, and metadata context.
|
||||
|
||||
@@ -196,11 +204,11 @@ STYLE & QUALITY RULES
|
||||
STAGE 3: METADATA CONTEXT (NEW)
|
||||
===========================
|
||||
|
||||
**Cluster Role:**
|
||||
[IGNY8_CLUSTER_ROLE]
|
||||
- If role is "hub": Create comprehensive, authoritative content that serves as the main resource for this topic cluster. Include overview sections, key concepts, and links to related topics.
|
||||
- If role is "supporting": Create detailed, focused content that supports the hub page. Dive deep into specific aspects, use cases, or subtopics.
|
||||
- If role is "attribute": Create content focused on specific attributes, features, or specifications. Include detailed comparisons, specifications, or attribute-focused information.
|
||||
**Content Structure:**
|
||||
[IGNY8_CONTENT_STRUCTURE]
|
||||
- If structure is "cluster_hub": Create comprehensive, authoritative content that serves as the main resource for this topic cluster. Include overview sections, key concepts, and links to related topics.
|
||||
- If structure is "article" or "guide": Create detailed, focused content that dives deep into the topic with actionable insights.
|
||||
- Other structures: Follow the appropriate format (listicle, comparison, review, landing_page, service_page, product_page, category_archive, tag_archive, attribute_archive).
|
||||
|
||||
**Taxonomy Context:**
|
||||
[IGNY8_TAXONOMY]
|
||||
|
||||
@@ -11,6 +11,34 @@ class Tasks(SiteSectorBaseModel):
|
||||
('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(
|
||||
@@ -34,16 +62,18 @@ class Tasks(SiteSectorBaseModel):
|
||||
content_type = models.CharField(
|
||||
max_length=100,
|
||||
db_index=True,
|
||||
help_text="Content type: post, page, product, service, category, tag, etc.",
|
||||
db_column='entity_type',
|
||||
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/format: article, listicle, guide, comparison, product_page, etc.",
|
||||
db_column='cluster_role',
|
||||
help_text="Content structure: article, guide, comparison, review, listicle, landing_page, etc.",
|
||||
choices=CONTENT_STRUCTURE_CHOICES,
|
||||
default='article',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
@@ -104,9 +134,37 @@ class Content(SiteSectorBaseModel):
|
||||
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", db_column='html_content')
|
||||
content_html = models.TextField(help_text="Final HTML content")
|
||||
cluster = models.ForeignKey(
|
||||
'planner.Clusters',
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -116,20 +174,18 @@ class Content(SiteSectorBaseModel):
|
||||
help_text="Parent cluster (required)"
|
||||
)
|
||||
content_type = models.CharField(
|
||||
max_length=100,
|
||||
max_length=50,
|
||||
choices=CONTENT_TYPE_CHOICES,
|
||||
default='post',
|
||||
db_index=True,
|
||||
help_text="Content type: post, page, product, service, category, tag, etc.",
|
||||
db_column='entity_type',
|
||||
blank=True,
|
||||
null=True
|
||||
help_text="Content type: post, page, product, taxonomy"
|
||||
)
|
||||
content_structure = models.CharField(
|
||||
max_length=100,
|
||||
max_length=50,
|
||||
choices=CONTENT_STRUCTURE_CHOICES,
|
||||
default='article',
|
||||
db_index=True,
|
||||
help_text="Content structure/format: article, listicle, guide, comparison, product_page, etc.",
|
||||
db_column='cluster_role',
|
||||
blank=True,
|
||||
null=True
|
||||
help_text="Content structure/format based on content type"
|
||||
)
|
||||
|
||||
# Taxonomy relationships
|
||||
|
||||
@@ -146,18 +146,32 @@ class ContentIdeas(SiteSectorBaseModel):
|
||||
('published', 'Published'),
|
||||
]
|
||||
|
||||
SITE_ENTITY_TYPE_CHOICES = [
|
||||
CONTENT_TYPE_CHOICES = [
|
||||
('post', 'Post'),
|
||||
('page', 'Page'),
|
||||
('product', 'Product'),
|
||||
('service', 'Service'),
|
||||
('taxonomy_term', 'Taxonomy Term'),
|
||||
('taxonomy', 'Taxonomy'),
|
||||
]
|
||||
|
||||
CLUSTER_ROLE_CHOICES = [
|
||||
('hub', 'Hub'),
|
||||
('supporting', 'Supporting'),
|
||||
('attribute', 'Attribute'),
|
||||
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'),
|
||||
]
|
||||
|
||||
idea_title = models.CharField(max_length=255, db_index=True)
|
||||
@@ -187,17 +201,17 @@ class ContentIdeas(SiteSectorBaseModel):
|
||||
)
|
||||
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='new')
|
||||
estimated_word_count = models.IntegerField(default=1000)
|
||||
site_entity_type = models.CharField(
|
||||
content_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=SITE_ENTITY_TYPE_CHOICES,
|
||||
default='page',
|
||||
help_text="Target entity type when promoting idea into tasks/pages"
|
||||
choices=CONTENT_TYPE_CHOICES,
|
||||
default='post',
|
||||
help_text="Content type: post, page, product, taxonomy"
|
||||
)
|
||||
cluster_role = models.CharField(
|
||||
content_structure = models.CharField(
|
||||
max_length=50,
|
||||
choices=CLUSTER_ROLE_CHOICES,
|
||||
default='hub',
|
||||
help_text="Role within the cluster-driven sitemap"
|
||||
choices=CONTENT_STRUCTURE_CHOICES,
|
||||
default='article',
|
||||
help_text="Content structure/format based on content type"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
@@ -212,8 +226,8 @@ class ContentIdeas(SiteSectorBaseModel):
|
||||
models.Index(fields=['idea_title']),
|
||||
models.Index(fields=['status']),
|
||||
models.Index(fields=['keyword_cluster']),
|
||||
models.Index(fields=['site_entity_type']),
|
||||
models.Index(fields=['cluster_role']),
|
||||
models.Index(fields=['content_type']),
|
||||
models.Index(fields=['content_structure']),
|
||||
models.Index(fields=['site', 'sector']),
|
||||
]
|
||||
|
||||
|
||||
@@ -125,10 +125,10 @@ class SitesRendererAdapter(BaseAdapter):
|
||||
# Get blocks from blueprint (placeholders)
|
||||
blocks = page.blocks_json or []
|
||||
page_metadata = {
|
||||
'entity_type': page.entity_type if hasattr(page, 'entity_type') else None,
|
||||
'content_type': page.content_type if hasattr(page, 'content_type') else None,
|
||||
'cluster_id': None,
|
||||
'cluster_name': None,
|
||||
'cluster_role': None,
|
||||
'content_structure': None,
|
||||
'taxonomy_id': None,
|
||||
'taxonomy_name': None,
|
||||
'internal_links': []
|
||||
@@ -178,7 +178,7 @@ class SitesRendererAdapter(BaseAdapter):
|
||||
if cluster_map and cluster_map.cluster:
|
||||
page_metadata['cluster_id'] = cluster_map.cluster.id
|
||||
page_metadata['cluster_name'] = cluster_map.cluster.name
|
||||
page_metadata['cluster_role'] = cluster_map.role or task.cluster_role if task else None
|
||||
page_metadata['content_structure'] = cluster_map.role or task.content_structure if task else None
|
||||
|
||||
# Get taxonomy mapping
|
||||
taxonomy_map = ContentTaxonomyMap.objects.filter(content=content).first()
|
||||
@@ -190,21 +190,21 @@ class SitesRendererAdapter(BaseAdapter):
|
||||
if content.internal_links:
|
||||
page_metadata['internal_links'] = content.internal_links
|
||||
|
||||
# Use content entity_type if available
|
||||
if content.entity_type:
|
||||
page_metadata['entity_type'] = content.entity_type
|
||||
# Use content_type if available
|
||||
if content.content_type:
|
||||
page_metadata['content_type'] = content.content_type
|
||||
|
||||
# Fallback to task metadata if content not found
|
||||
if task and not page_metadata.get('cluster_id'):
|
||||
if task.cluster:
|
||||
page_metadata['cluster_id'] = task.cluster.id
|
||||
page_metadata['cluster_name'] = task.cluster.name
|
||||
page_metadata['cluster_role'] = task.cluster_role
|
||||
page_metadata['content_structure'] = task.content_structure
|
||||
if task.taxonomy:
|
||||
page_metadata['taxonomy_id'] = task.taxonomy.id
|
||||
page_metadata['taxonomy_name'] = task.taxonomy.name
|
||||
if task.entity_type:
|
||||
page_metadata['entity_type'] = task.entity_type
|
||||
if task.content_type:
|
||||
page_metadata['content_type'] = task.content_type
|
||||
|
||||
pages.append({
|
||||
'id': page.id,
|
||||
|
||||
@@ -235,19 +235,19 @@ class PageGenerationService:
|
||||
'contact': 'page',
|
||||
'custom': 'page',
|
||||
}
|
||||
entity_type = entity_type_map.get(page_blueprint.type, 'page')
|
||||
content_type = entity_type_map.get(page_blueprint.type, 'page')
|
||||
|
||||
# Stage 3: Try to find related cluster and taxonomy from blueprint
|
||||
cluster_role = 'hub' # Default
|
||||
# Try to find related cluster and taxonomy from blueprint
|
||||
content_structure = 'article' # Default
|
||||
taxonomy = None
|
||||
|
||||
# Find cluster link for this blueprint to infer role
|
||||
# Find cluster link for this blueprint to infer structure
|
||||
from igny8_core.business.site_building.models import SiteBlueprintCluster
|
||||
cluster_link = SiteBlueprintCluster.objects.filter(
|
||||
site_blueprint=page_blueprint.site_blueprint
|
||||
).first()
|
||||
if cluster_link:
|
||||
cluster_role = cluster_link.role
|
||||
content_structure = cluster_link.role or 'article'
|
||||
|
||||
# Find taxonomy if page type suggests it (products/services)
|
||||
if page_blueprint.type in ['products', 'services']:
|
||||
@@ -264,13 +264,10 @@ class PageGenerationService:
|
||||
title=title,
|
||||
description="\n".join(filter(None, description_parts)),
|
||||
keywords=keywords,
|
||||
content_structure=self._map_content_structure(page_blueprint.type),
|
||||
content_type='article',
|
||||
content_structure=self._map_content_structure(page_blueprint.type) or content_structure,
|
||||
content_type=content_type,
|
||||
status='queued',
|
||||
# Stage 3: Set entity metadata
|
||||
entity_type=entity_type,
|
||||
taxonomy=taxonomy,
|
||||
cluster_role=cluster_role,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -59,8 +59,8 @@ class KeywordsAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
|
||||
@admin.register(ContentIdeas)
|
||||
class ContentIdeasAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
list_display = ['idea_title', 'site', 'sector', 'description_preview', 'site_entity_type', 'cluster_role', 'status', 'keyword_cluster', 'estimated_word_count', 'created_at']
|
||||
list_filter = ['status', 'site_entity_type', 'cluster_role', 'site', 'sector']
|
||||
list_display = ['idea_title', 'site', 'sector', 'description_preview', 'content_type', 'content_structure', 'status', 'keyword_cluster', 'estimated_word_count', 'created_at']
|
||||
list_filter = ['status', 'content_type', 'content_structure', 'site', 'sector']
|
||||
search_fields = ['idea_title', 'target_keywords', 'description']
|
||||
ordering = ['-created_at']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
@@ -70,7 +70,7 @@ class ContentIdeasAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
'fields': ('idea_title', 'description', 'status', 'site', 'sector')
|
||||
}),
|
||||
('Content Planning', {
|
||||
'fields': ('site_entity_type', 'cluster_role', 'estimated_word_count')
|
||||
'fields': ('content_type', 'content_structure', 'estimated_word_count')
|
||||
}),
|
||||
('Keywords & Clustering', {
|
||||
'fields': ('keyword_cluster', 'target_keywords', 'taxonomy')
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-26 14:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('igny8_core_auth', '0002_add_wp_api_key_to_site'),
|
||||
('planner', '0004_remove_clusters_igny8_clust_context_0d6bd7_idx_and_more'),
|
||||
('site_building', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveIndex(
|
||||
model_name='contentideas',
|
||||
name='igny8_conte_site_en_511349_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='contentideas',
|
||||
name='igny8_conte_cluster_234240_idx',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contentideas',
|
||||
name='cluster_role',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contentideas',
|
||||
name='site_entity_type',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contentideas',
|
||||
name='content_structure',
|
||||
field=models.CharField(choices=[('article', 'Article'), ('guide', 'Guide'), ('comparison', 'Comparison'), ('review', 'Review'), ('listicle', 'Listicle'), ('landing_page', 'Landing Page'), ('business_page', 'Business Page'), ('service_page', 'Service Page'), ('general', 'General'), ('cluster_hub', 'Cluster Hub'), ('product_page', 'Product Page'), ('category_archive', 'Category Archive'), ('tag_archive', 'Tag Archive'), ('attribute_archive', 'Attribute Archive')], default='article', help_text='Content structure/format based on content type', max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contentideas',
|
||||
name='content_type',
|
||||
field=models.CharField(choices=[('post', 'Post'), ('page', 'Page'), ('product', 'Product'), ('taxonomy', 'Taxonomy')], default='post', help_text='Content type: post, page, product, taxonomy', max_length=50),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='contentideas',
|
||||
index=models.Index(fields=['content_type'], name='igny8_conte_content_e74415_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='contentideas',
|
||||
index=models.Index(fields=['content_structure'], name='igny8_conte_content_3eede7_idx'),
|
||||
),
|
||||
]
|
||||
@@ -171,8 +171,8 @@ class ContentIdeasSerializer(serializers.ModelSerializer):
|
||||
'id',
|
||||
'idea_title',
|
||||
'description',
|
||||
'site_entity_type',
|
||||
'cluster_role',
|
||||
'content_type',
|
||||
'content_structure',
|
||||
'target_keywords',
|
||||
'keyword_cluster_id',
|
||||
'keyword_cluster_name',
|
||||
|
||||
@@ -927,7 +927,7 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
|
||||
ordering = ['-created_at'] # Default ordering (newest first)
|
||||
|
||||
# Filter configuration (updated for new structure)
|
||||
filterset_fields = ['status', 'keyword_cluster_id', 'site_entity_type', 'cluster_role']
|
||||
filterset_fields = ['status', 'keyword_cluster_id', 'content_type', 'content_structure']
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Require explicit site_id and sector_id - no defaults."""
|
||||
@@ -1013,27 +1013,13 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
|
||||
errors = []
|
||||
for idea in ideas:
|
||||
try:
|
||||
# STAGE 3: Map idea fields to final Task schema
|
||||
# Map site_entity_type → content_type (with fallback)
|
||||
content_type = idea.site_entity_type if idea.site_entity_type else 'post'
|
||||
|
||||
# Map cluster_role → content_structure (with fallback)
|
||||
# hub → article, supporting → guide, attribute → comparison
|
||||
role_to_structure = {
|
||||
'hub': 'article',
|
||||
'supporting': 'guide',
|
||||
'attribute': 'comparison',
|
||||
}
|
||||
cluster_role = idea.cluster_role if idea.cluster_role else 'hub'
|
||||
content_structure = role_to_structure.get(cluster_role, 'article')
|
||||
|
||||
# Create task with Stage 1 final fields
|
||||
# Direct copy - no mapping needed
|
||||
task = Tasks.objects.create(
|
||||
title=idea.idea_title,
|
||||
description=idea.description or '',
|
||||
cluster=idea.keyword_cluster,
|
||||
content_type=content_type,
|
||||
content_structure=content_structure,
|
||||
content_type=idea.content_type or 'post',
|
||||
content_structure=idea.content_structure or 'article',
|
||||
taxonomy_term=None, # Can be set later if taxonomy is available
|
||||
status='queued',
|
||||
account=idea.account,
|
||||
@@ -1056,7 +1042,7 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
|
||||
if errors:
|
||||
return error_response(
|
||||
error=f'Failed to create {len(errors)} tasks',
|
||||
details=errors,
|
||||
errors=errors,
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
@@ -228,10 +228,10 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet):
|
||||
).values_list('content_id', flat=True).distinct()
|
||||
cluster_content = content.filter(id__in=cluster_content_ids)
|
||||
|
||||
# Count by role
|
||||
hub_count = cluster_tasks.filter(cluster_role='hub').count()
|
||||
supporting_count = cluster_tasks.filter(cluster_role='supporting').count()
|
||||
attribute_count = cluster_tasks.filter(cluster_role='attribute').count()
|
||||
# Count by structure
|
||||
hub_count = cluster_tasks.filter(content_structure='cluster_hub').count()
|
||||
supporting_count = cluster_tasks.filter(content_structure__in=['article', 'guide', 'comparison']).count()
|
||||
attribute_count = cluster_tasks.filter(content_structure='attribute_archive').count()
|
||||
|
||||
cluster_progress.append({
|
||||
'cluster_id': cluster.id,
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Generated manually for field rename implementation
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('planner', '0005_field_rename_implementation'),
|
||||
('writer', '0007_alter_contenttaxonomyrelation_unique_together_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Rename database columns for Tasks
|
||||
migrations.RunSQL(
|
||||
sql="""
|
||||
-- Rename Tasks columns (only if they exist)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'igny8_tasks' AND column_name = 'entity_type') THEN
|
||||
ALTER TABLE igny8_tasks RENAME COLUMN entity_type TO content_type;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'igny8_tasks' AND column_name = 'cluster_role') THEN
|
||||
ALTER TABLE igny8_tasks RENAME COLUMN cluster_role TO content_structure;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Drop old indexes if they exist
|
||||
DROP INDEX IF EXISTS igny8_tasks_entity__1dc185_idx;
|
||||
DROP INDEX IF EXISTS igny8_tasks_cluster_c87903_idx;
|
||||
|
||||
-- Create new indexes
|
||||
CREATE INDEX IF NOT EXISTS igny8_tasks_content_type_idx ON igny8_tasks(content_type);
|
||||
CREATE INDEX IF NOT EXISTS igny8_tasks_content_structure_idx ON igny8_tasks(content_structure);
|
||||
""",
|
||||
reverse_sql="""
|
||||
ALTER TABLE igny8_tasks RENAME COLUMN content_type TO entity_type;
|
||||
ALTER TABLE igny8_tasks RENAME COLUMN content_structure TO cluster_role;
|
||||
DROP INDEX IF EXISTS igny8_tasks_content_type_idx;
|
||||
DROP INDEX IF EXISTS igny8_tasks_content_structure_idx;
|
||||
CREATE INDEX igny8_tasks_entity__1dc185_idx ON igny8_tasks(entity_type);
|
||||
CREATE INDEX igny8_tasks_cluster_c87903_idx ON igny8_tasks(cluster_role);
|
||||
"""
|
||||
),
|
||||
|
||||
# Rename database columns for Content
|
||||
migrations.RunSQL(
|
||||
sql="""
|
||||
-- Rename Content columns (only if they exist)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'igny8_content' AND column_name = 'entity_type') THEN
|
||||
ALTER TABLE igny8_content RENAME COLUMN entity_type TO content_type;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'igny8_content' AND column_name = 'cluster_role') THEN
|
||||
ALTER TABLE igny8_content RENAME COLUMN cluster_role TO content_structure;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'igny8_content' AND column_name = 'html_content') THEN
|
||||
ALTER TABLE igny8_content RENAME COLUMN html_content TO content_html;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Drop old indexes if they exist
|
||||
DROP INDEX IF EXISTS igny8_conte_entity__f559b3_idx;
|
||||
DROP INDEX IF EXISTS igny8_conte_cluster_32e22a_idx;
|
||||
|
||||
-- Create new indexes
|
||||
CREATE INDEX IF NOT EXISTS igny8_content_content_type_idx ON igny8_content(content_type);
|
||||
CREATE INDEX IF NOT EXISTS igny8_content_content_structure_idx ON igny8_content(content_structure);
|
||||
""",
|
||||
reverse_sql="""
|
||||
ALTER TABLE igny8_content RENAME COLUMN content_type TO entity_type;
|
||||
ALTER TABLE igny8_content RENAME COLUMN content_structure TO cluster_role;
|
||||
ALTER TABLE igny8_content RENAME COLUMN content_html TO html_content;
|
||||
DROP INDEX IF EXISTS igny8_content_content_type_idx;
|
||||
DROP INDEX IF EXISTS igny8_content_content_structure_idx;
|
||||
CREATE INDEX igny8_conte_entity__f559b3_idx ON igny8_content(entity_type);
|
||||
CREATE INDEX igny8_conte_cluster_32e22a_idx ON igny8_content(cluster_role);
|
||||
"""
|
||||
),
|
||||
|
||||
# Rename database columns for ContentTaxonomyMap
|
||||
migrations.RunSQL(
|
||||
sql="""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'igny8_content_taxonomy_map' AND column_name = 'entity_type') THEN
|
||||
ALTER TABLE igny8_content_taxonomy_map RENAME COLUMN entity_type TO content_type;
|
||||
END IF;
|
||||
END $$;
|
||||
""",
|
||||
reverse_sql="""
|
||||
ALTER TABLE igny8_content_taxonomy_map RENAME COLUMN content_type TO entity_type;
|
||||
"""
|
||||
),
|
||||
|
||||
# Rename database columns for TaxonomyTerms
|
||||
migrations.RunSQL(
|
||||
sql="""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'igny8_taxonomy_terms' AND column_name = 'entity_type') THEN
|
||||
ALTER TABLE igny8_taxonomy_terms RENAME COLUMN entity_type TO content_type;
|
||||
END IF;
|
||||
END $$;
|
||||
""",
|
||||
reverse_sql="""
|
||||
ALTER TABLE igny8_taxonomy_terms RENAME COLUMN content_type TO entity_type;
|
||||
"""
|
||||
),
|
||||
]
|
||||
53
backend/rename_fields_migration.sql
Normal file
53
backend/rename_fields_migration.sql
Normal file
@@ -0,0 +1,53 @@
|
||||
-- COMPREHENSIVE FIELD RENAME MIGRATION
|
||||
-- Renames all entity_type, cluster_role, site_entity_type columns to content_type and content_structure
|
||||
-- Date: 2025-11-26
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1. ContentIdeas table (igny8_content_ideas)
|
||||
ALTER TABLE igny8_content_ideas RENAME COLUMN site_entity_type TO content_type;
|
||||
ALTER TABLE igny8_content_ideas RENAME COLUMN cluster_role TO content_structure;
|
||||
|
||||
-- Update index names for ContentIdeas
|
||||
DROP INDEX IF EXISTS igny8_content_ideas_site_entity_type_idx;
|
||||
DROP INDEX IF EXISTS igny8_content_ideas_cluster_role_idx;
|
||||
CREATE INDEX igny8_content_ideas_content_type_idx ON igny8_content_ideas(content_type);
|
||||
CREATE INDEX igny8_content_ideas_content_structure_idx ON igny8_content_ideas(content_structure);
|
||||
|
||||
-- 2. Tasks table (igny8_tasks)
|
||||
ALTER TABLE igny8_tasks RENAME COLUMN entity_type TO content_type;
|
||||
-- cluster_role already mapped via db_column, but let's check if column exists
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'igny8_tasks' AND column_name = 'cluster_role') THEN
|
||||
ALTER TABLE igny8_tasks RENAME COLUMN cluster_role TO content_structure;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 3. Content table (igny8_content)
|
||||
ALTER TABLE igny8_content RENAME COLUMN entity_type TO content_type;
|
||||
-- cluster_role already mapped via db_column, but let's check if column exists
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'igny8_content' AND column_name = 'cluster_role') THEN
|
||||
ALTER TABLE igny8_content RENAME COLUMN cluster_role TO content_structure;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 4. ContentTaxonomy table (igny8_content_taxonomy)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'igny8_content_taxonomy' AND column_name = 'entity_type') THEN
|
||||
ALTER TABLE igny8_content_taxonomy RENAME COLUMN entity_type TO content_type;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 5. AITaskExecution table (igny8_ai_task_execution)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'igny8_ai_task_execution' AND column_name = 'entity_type') THEN
|
||||
ALTER TABLE igny8_ai_task_execution RENAME COLUMN entity_type TO content_type;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
138
backend/test_field_rename.py
Normal file
138
backend/test_field_rename.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Test script to verify field rename is complete
|
||||
Run after SQL migration: python manage.py shell < test_field_rename.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import django
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings')
|
||||
django.setup()
|
||||
|
||||
from igny8_core.business.planning.models import ContentIdeas
|
||||
from igny8_core.business.content.models import Tasks, Content
|
||||
from django.db import connection
|
||||
|
||||
def test_models():
|
||||
"""Test that models can access new field names"""
|
||||
print("\n=== Testing Model Field Access ===")
|
||||
|
||||
# Test ContentIdeas
|
||||
try:
|
||||
idea = ContentIdeas.objects.first()
|
||||
if idea:
|
||||
print(f"✓ ContentIdeas.content_type: {idea.content_type}")
|
||||
print(f"✓ ContentIdeas.content_structure: {idea.content_structure}")
|
||||
else:
|
||||
print("⚠ No ContentIdeas records to test")
|
||||
except Exception as e:
|
||||
print(f"✗ ContentIdeas error: {e}")
|
||||
|
||||
# Test Tasks
|
||||
try:
|
||||
task = Tasks.objects.first()
|
||||
if task:
|
||||
print(f"✓ Tasks.content_type: {task.content_type}")
|
||||
print(f"✓ Tasks.content_structure: {task.content_structure}")
|
||||
else:
|
||||
print("⚠ No Tasks records to test")
|
||||
except Exception as e:
|
||||
print(f"✗ Tasks error: {e}")
|
||||
|
||||
# Test Content
|
||||
try:
|
||||
content = Content.objects.first()
|
||||
if content:
|
||||
print(f"✓ Content.content_type: {content.content_type}")
|
||||
print(f"✓ Content.content_structure: {content.content_structure}")
|
||||
print(f"✓ Content.content_html: {len(content.content_html) if content.content_html else 0} chars")
|
||||
else:
|
||||
print("⚠ No Content records to test")
|
||||
except Exception as e:
|
||||
print(f"✗ Content error: {e}")
|
||||
|
||||
def test_database_columns():
|
||||
"""Verify database columns exist"""
|
||||
print("\n=== Testing Database Column Names ===")
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
# Check igny8_content_ideas
|
||||
cursor.execute("""
|
||||
SELECT column_name FROM information_schema.columns
|
||||
WHERE table_name = 'igny8_content_ideas'
|
||||
AND column_name IN ('content_type', 'content_structure', 'site_entity_type', 'cluster_role')
|
||||
ORDER BY column_name
|
||||
""")
|
||||
cols = [row[0] for row in cursor.fetchall()]
|
||||
print(f"igny8_content_ideas columns: {cols}")
|
||||
if 'content_type' in cols and 'content_structure' in cols:
|
||||
print("✓ ContentIdeas: new columns exist")
|
||||
if 'site_entity_type' in cols or 'cluster_role' in cols:
|
||||
print("✗ ContentIdeas: old columns still exist")
|
||||
|
||||
# Check igny8_tasks
|
||||
cursor.execute("""
|
||||
SELECT column_name FROM information_schema.columns
|
||||
WHERE table_name = 'igny8_tasks'
|
||||
AND column_name IN ('content_type', 'content_structure', 'entity_type', 'cluster_role')
|
||||
ORDER BY column_name
|
||||
""")
|
||||
cols = [row[0] for row in cursor.fetchall()]
|
||||
print(f"igny8_tasks columns: {cols}")
|
||||
if 'content_type' in cols and 'content_structure' in cols:
|
||||
print("✓ Tasks: new columns exist")
|
||||
if 'entity_type' in cols or 'cluster_role' in cols:
|
||||
print("✗ Tasks: old columns still exist")
|
||||
|
||||
# Check igny8_content
|
||||
cursor.execute("""
|
||||
SELECT column_name FROM information_schema.columns
|
||||
WHERE table_name = 'igny8_content'
|
||||
AND column_name IN ('content_type', 'content_structure', 'content_html', 'entity_type', 'cluster_role', 'html_content')
|
||||
ORDER BY column_name
|
||||
""")
|
||||
cols = [row[0] for row in cursor.fetchall()]
|
||||
print(f"igny8_content columns: {cols}")
|
||||
if 'content_type' in cols and 'content_structure' in cols and 'content_html' in cols:
|
||||
print("✓ Content: new columns exist")
|
||||
if 'entity_type' in cols or 'cluster_role' in cols or 'html_content' in cols:
|
||||
print("✗ Content: old columns still exist")
|
||||
|
||||
def test_choices():
|
||||
"""Test that CHOICES are correctly defined"""
|
||||
print("\n=== Testing Model CHOICES ===")
|
||||
|
||||
print(f"ContentIdeas.CONTENT_TYPE_CHOICES: {len(ContentIdeas.CONTENT_TYPE_CHOICES)} types")
|
||||
print(f"ContentIdeas.CONTENT_STRUCTURE_CHOICES: {len(ContentIdeas.CONTENT_STRUCTURE_CHOICES)} structures")
|
||||
|
||||
print(f"Tasks.CONTENT_TYPE_CHOICES: {len(Tasks.CONTENT_TYPE_CHOICES)} types")
|
||||
print(f"Tasks.CONTENT_STRUCTURE_CHOICES: {len(Tasks.CONTENT_STRUCTURE_CHOICES)} structures")
|
||||
|
||||
print(f"Content.CONTENT_TYPE_CHOICES: {len(Content.CONTENT_TYPE_CHOICES)} types")
|
||||
print(f"Content.CONTENT_STRUCTURE_CHOICES: {len(Content.CONTENT_STRUCTURE_CHOICES)} structures")
|
||||
|
||||
# Verify all 14 structures are present
|
||||
all_structures = [s[0] for s in Tasks.CONTENT_STRUCTURE_CHOICES]
|
||||
expected = ['article', 'guide', 'comparison', 'review', 'listicle',
|
||||
'landing_page', 'business_page', 'service_page', 'general', 'cluster_hub',
|
||||
'product_page', 'category_archive', 'tag_archive', 'attribute_archive']
|
||||
|
||||
for struct in expected:
|
||||
if struct in all_structures:
|
||||
print(f"✓ {struct}")
|
||||
else:
|
||||
print(f"✗ Missing: {struct}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("="*60)
|
||||
print("Field Rename Validation Test")
|
||||
print("="*60)
|
||||
|
||||
test_models()
|
||||
test_database_columns()
|
||||
test_choices()
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("Test Complete")
|
||||
print("="*60)
|
||||
Reference in New Issue
Block a user