This commit is contained in:
IGNY8 VPS (Salman)
2025-11-22 01:13:25 +00:00
parent 3735f99207
commit c84bb9bc14
10 changed files with 730 additions and 90 deletions

View File

@@ -8,21 +8,23 @@ class Tasks(SiteSectorBaseModel):
STATUS_CHOICES = [
('queued', 'Queued'),
('in_progress', 'In Progress'),
('completed', 'Completed'),
('failed', 'Failed'),
]
CONTENT_STRUCTURE_CHOICES = [
('cluster_hub', 'Cluster Hub'),
('landing_page', 'Landing Page'),
('pillar_page', 'Pillar Page'),
('supporting_page', 'Supporting Page'),
ENTITY_TYPE_CHOICES = [
('post', 'Post'),
('page', 'Page'),
('product', 'Product'),
('service', 'Service'),
('taxonomy_term', 'Taxonomy Term'),
]
CONTENT_TYPE_CHOICES = [
('blog_post', 'Blog Post'),
('article', 'Article'),
('guide', 'Guide'),
('tutorial', 'Tutorial'),
CLUSTER_ROLE_CHOICES = [
('hub', 'Hub'),
('supporting', 'Supporting'),
('attribute', 'Attribute'),
]
title = models.CharField(max_length=255, db_index=True)
@@ -49,32 +51,13 @@ class Tasks(SiteSectorBaseModel):
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')
# Stage 3: Entity metadata fields
ENTITY_TYPE_CHOICES = [
('blog_post', 'Blog Post'),
('article', 'Article'),
('product', 'Product'),
('service', 'Service Page'),
('taxonomy', 'Taxonomy Page'),
('page', 'Page'),
]
CLUSTER_ROLE_CHOICES = [
('hub', 'Hub Page'),
('supporting', 'Supporting Page'),
('attribute', 'Attribute Page'),
]
entity_type = models.CharField(
max_length=50,
choices=ENTITY_TYPE_CHOICES,
default='blog_post',
default='post',
db_index=True,
blank=True,
null=True,
help_text="Type of content entity (inherited from idea/blueprint)"
help_text="Type of content entity"
)
taxonomy = models.ForeignKey(
'site_building.SiteBlueprintTaxonomy',
@@ -88,22 +71,9 @@ class Tasks(SiteSectorBaseModel):
max_length=50,
choices=CLUSTER_ROLE_CHOICES,
default='hub',
blank=True,
null=True,
help_text="Role within the cluster-driven sitemap"
)
# 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)
@@ -117,7 +87,6 @@ class Tasks(SiteSectorBaseModel):
models.Index(fields=['title']),
models.Index(fields=['status']),
models.Index(fields=['cluster']),
models.Index(fields=['content_type']),
models.Index(fields=['entity_type']),
models.Index(fields=['cluster_role']),
models.Index(fields=['site', 'sector']),
@@ -148,8 +117,7 @@ class Content(SiteSectorBaseModel):
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'),

View File

@@ -164,38 +164,22 @@ class ContentIdeas(SiteSectorBaseModel):
('published', 'Published'),
]
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'),
]
SITE_ENTITY_TYPE_CHOICES = [
('page', 'Site Page'),
('blog_post', 'Blog Post'),
('post', 'Post'),
('page', 'Page'),
('product', 'Product'),
('service', 'Service'),
('taxonomy', 'Taxonomy Page'),
('taxonomy_term', 'Taxonomy Term'),
]
CLUSTER_ROLE_CHOICES = [
('hub', 'Hub Page'),
('supporting', 'Supporting Page'),
('attribute', 'Attribute Page'),
('hub', 'Hub'),
('supporting', 'Supporting'),
('attribute', 'Attribute'),
]
idea_title = models.CharField(max_length=255, db_index=True)
description = models.TextField(blank=True, null=True)
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')
target_keywords = models.CharField(max_length=500, blank=True) # Comma-separated keywords (legacy)
keyword_objects = models.ManyToManyField(
'Keywords',
@@ -246,7 +230,6 @@ class ContentIdeas(SiteSectorBaseModel):
models.Index(fields=['idea_title']),
models.Index(fields=['status']),
models.Index(fields=['keyword_cluster']),
models.Index(fields=['content_structure']),
models.Index(fields=['site_entity_type']),
models.Index(fields=['cluster_role']),
models.Index(fields=['site', 'sector']),

View File

@@ -168,6 +168,98 @@ class IntegrationViewSet(SiteSectorModelViewSet):
return success_response(status_data, request=request)
@action(detail=True, methods=['get'], url_path='content-types')
def content_types_summary(self, request, pk=None):
"""
Get content types summary with counts from synced data.
GET /api/v1/integration/integrations/{id}/content-types/
Returns:
{
"success": true,
"data": {
"post_types": {
"post": {"label": "Posts", "count": 123, "synced_count": 50},
"page": {"label": "Pages", "count": 12, "synced_count": 12},
"product": {"label": "Products", "count": 456, "synced_count": 200}
},
"taxonomies": {
"category": {"label": "Categories", "count": 25, "synced_count": 25},
"post_tag": {"label": "Tags", "count": 102, "synced_count": 80},
"product_cat": {"label": "Product Categories", "count": 15, "synced_count": 15}
},
"last_structure_fetch": "2025-11-22T10:00:00Z"
}
}
"""
integration = self.get_object()
site = integration.site
# Get config from integration
config = integration.config_json or {}
content_types = config.get('content_types', {})
# Get synced counts from Content and ContentTaxonomy models
from igny8_core.business.content.models import Content, ContentTaxonomy
# Build response with synced counts
post_types_data = {}
for wp_type, type_config in content_types.get('post_types', {}).items():
# Map WP type to entity_type
entity_type_map = {
'post': 'post',
'page': 'page',
'product': 'product',
'service': 'service',
}
entity_type = entity_type_map.get(wp_type, 'post')
# Count synced content
synced_count = Content.objects.filter(
site=site,
entity_type=entity_type,
external_type=wp_type,
sync_status__in=['imported', 'synced']
).count()
post_types_data[wp_type] = {
'label': type_config.get('label', wp_type.title()),
'count': type_config.get('count', 0),
'synced_count': synced_count,
'enabled': type_config.get('enabled', False),
'fetch_limit': type_config.get('fetch_limit', 100),
'last_synced': type_config.get('last_synced'),
}
taxonomies_data = {}
for wp_tax, tax_config in content_types.get('taxonomies', {}).items():
# Count synced taxonomies
synced_count = ContentTaxonomy.objects.filter(
site=site,
external_taxonomy=wp_tax,
sync_status__in=['imported', 'synced']
).count()
taxonomies_data[wp_tax] = {
'label': tax_config.get('label', wp_tax.title()),
'count': tax_config.get('count', 0),
'synced_count': synced_count,
'enabled': tax_config.get('enabled', False),
'fetch_limit': tax_config.get('fetch_limit', 100),
'last_synced': tax_config.get('last_synced'),
}
summary = {
'post_types': post_types_data,
'taxonomies': taxonomies_data,
'last_structure_fetch': config.get('last_structure_fetch'),
'plugin_connection_enabled': config.get('plugin_connection_enabled', True),
'two_way_sync_enabled': config.get('two_way_sync_enabled', True),
}
return success_response(summary, request=request)
# Stage 4: Site-level sync endpoints
@action(detail=False, methods=['get'], url_path='sites/(?P<site_id>[^/.]+)/sync/status')

View File

@@ -63,7 +63,7 @@ class ContentIdeasAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
list_filter = ['status', 'site_entity_type', 'cluster_role', 'site', 'sector']
search_fields = ['idea_title', 'target_keywords', 'description']
ordering = ['-created_at']
readonly_fields = ['content_structure', 'content_type']
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Basic Info', {
@@ -75,10 +75,9 @@ class ContentIdeasAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
('Keywords & Clustering', {
'fields': ('keyword_cluster', 'target_keywords', 'taxonomy')
}),
('Deprecated Fields (Read-Only)', {
'fields': ('content_structure', 'content_type'),
'classes': ('collapse',),
'description': 'These fields are deprecated. Use site_entity_type and cluster_role instead.'
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)

View File

@@ -0,0 +1,22 @@
# Generated migration to remove deprecated fields from ContentIdeas
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('planner', '0002_initial'),
]
operations = [
# Remove deprecated fields from ContentIdeas
migrations.RemoveField(
model_name='contentideas',
name='content_structure',
),
migrations.RemoveField(
model_name='contentideas',
name='content_type',
),
]

View File

@@ -6,23 +6,25 @@ from igny8_core.business.content.models import ContentTaxonomy, ContentAttribute
@admin.register(Tasks)
class TasksAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
list_display = ['title', 'site', 'sector', 'status', 'cluster', 'created_at']
list_filter = ['status', 'site', 'sector', 'cluster']
list_display = ['title', 'entity_type', 'cluster_role', 'site', 'sector', 'status', 'cluster', 'created_at']
list_filter = ['status', 'entity_type', 'cluster_role', 'site', 'sector', 'cluster']
search_fields = ['title', 'description', 'keywords']
ordering = ['-created_at']
readonly_fields = ['content_type', 'content_structure', 'entity_type', 'cluster_role', 'assigned_post_id', 'post_url']
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Basic Info', {
'fields': ('title', 'description', 'status', 'site', 'sector')
}),
('Content Classification', {
'fields': ('entity_type', 'cluster_role', 'taxonomy')
}),
('Planning', {
'fields': ('cluster', 'idea', 'keywords')
}),
('Deprecated Fields (Read-Only)', {
'fields': ('content_type', 'content_structure', 'entity_type', 'cluster_role', 'assigned_post_id', 'post_url'),
'classes': ('collapse',),
'description': 'These fields are deprecated. Use Content model instead.'
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
@@ -88,7 +90,7 @@ class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
list_filter = ['entity_type', 'content_format', 'cluster_role', 'source', 'sync_status', 'status', 'site', 'sector', 'generated_at']
search_fields = ['title', 'meta_title', 'primary_keyword', 'task__title', 'external_url']
ordering = ['-generated_at']
readonly_fields = ['categories', 'tags']
readonly_fields = ['generated_at', 'updated_at']
fieldsets = (
('Basic Info', {
@@ -111,10 +113,9 @@ class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
'fields': ('linker_version', 'optimizer_version', 'optimization_scores', 'internal_links'),
'classes': ('collapse',)
}),
('Deprecated Fields (Read-Only)', {
'fields': ('categories', 'tags'),
'classes': ('collapse',),
'description': 'These fields are deprecated. Use taxonomies M2M instead.'
('Timestamps', {
'fields': ('generated_at', 'updated_at'),
'classes': ('collapse',)
}),
)

View File

@@ -0,0 +1,93 @@
# Generated migration to clean up deprecated fields
from django.db import migrations, models
def migrate_deprecated_data(apps, schema_editor):
"""Migrate data from deprecated fields to new unified structure"""
Tasks = apps.get_model('writer', 'Tasks')
Content = apps.get_model('writer', 'Content')
# Migrate Tasks: ensure entity_type and cluster_role have defaults
for task in Tasks.objects.all():
changed = False
if not task.entity_type:
task.entity_type = 'post'
changed = True
if not task.cluster_role:
task.cluster_role = 'hub'
changed = True
if changed:
task.save()
# Migrate Content: ensure entity_type is set from task if available
for content in Content.objects.select_related('task').all():
changed = False
if content.task and content.task.entity_type and not content.entity_type:
content.entity_type = content.task.entity_type
changed = True
if content.task and content.task.cluster_role and not content.cluster_role:
content.cluster_role = content.task.cluster_role
changed = True
if not content.entity_type:
content.entity_type = 'post'
changed = True
if changed:
content.save()
class Migration(migrations.Migration):
dependencies = [
('writer', '0005_phase3_mark_deprecated_fields'),
('planner', '0003_cleanup_remove_deprecated_fields'),
]
operations = [
# Step 1: Migrate data
migrations.RunPython(migrate_deprecated_data, migrations.RunPython.noop),
# Step 2: Remove deprecated fields from Tasks
migrations.RemoveField(
model_name='tasks',
name='content_structure',
),
migrations.RemoveField(
model_name='tasks',
name='content_type',
),
migrations.RemoveField(
model_name='tasks',
name='content',
),
migrations.RemoveField(
model_name='tasks',
name='word_count',
),
migrations.RemoveField(
model_name='tasks',
name='meta_title',
),
migrations.RemoveField(
model_name='tasks',
name='meta_description',
),
migrations.RemoveField(
model_name='tasks',
name='assigned_post_id',
),
migrations.RemoveField(
model_name='tasks',
name='post_url',
),
# Step 4: Remove deprecated fields from Content
migrations.RemoveField(
model_name='content',
name='categories',
),
migrations.RemoveField(
model_name='content',
name='tags',
),
]

View File

@@ -52,11 +52,11 @@ class TasksViewSet(SiteSectorModelViewSet):
search_fields = ['title', 'keywords']
# Ordering configuration
ordering_fields = ['title', 'created_at', 'word_count', 'status']
ordering_fields = ['title', 'created_at', 'status']
ordering = ['-created_at'] # Default ordering (newest first)
# Filter configuration (removed deprecated fields)
filterset_fields = ['status', 'cluster_id']
# Filter configuration
filterset_fields = ['status', 'entity_type', 'cluster_role', 'cluster_id']
def perform_create(self, serializer):
"""Require explicit site_id and sector_id - no defaults."""