Stage 1 migration and docs complete

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-25 16:12:01 +00:00
parent f63ce92587
commit d19ea662ea
15 changed files with 764 additions and 2218 deletions

View File

@@ -0,0 +1,39 @@
# Generated by Django 5.2.8 on 2025-11-25 15:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('planner', '0003_cleanup_remove_deprecated_fields'),
]
operations = [
migrations.RemoveIndex(
model_name='clusters',
name='igny8_clust_context_0d6bd7_idx',
),
migrations.RemoveIndex(
model_name='contentideas',
name='igny8_conte_content_3eede7_idx',
),
migrations.RemoveField(
model_name='clusters',
name='context_type',
),
migrations.RemoveField(
model_name='clusters',
name='dimension_meta',
),
migrations.AlterField(
model_name='contentideas',
name='cluster_role',
field=models.CharField(choices=[('hub', 'Hub'), ('supporting', 'Supporting'), ('attribute', 'Attribute')], default='hub', help_text='Role within the cluster-driven sitemap', max_length=50),
),
migrations.AlterField(
model_name='contentideas',
name='site_entity_type',
field=models.CharField(choices=[('post', 'Post'), ('page', 'Page'), ('product', 'Product'), ('service', 'Service'), ('taxonomy_term', 'Taxonomy Term')], default='page', help_text='Target entity type when promoting idea into tasks/pages', max_length=50),
),
]

View File

@@ -6,9 +6,9 @@ from igny8_core.business.content.models import ContentTaxonomy, ContentAttribute
@admin.register(Tasks)
class TasksAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
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']
list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'status', 'cluster', 'created_at']
list_filter = ['status', 'content_type', 'content_structure', 'site', 'sector', 'cluster']
search_fields = ['title', 'description']
ordering = ['-created_at']
readonly_fields = ['created_at', 'updated_at']
@@ -17,10 +17,10 @@ class TasksAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
'fields': ('title', 'description', 'status', 'site', 'sector')
}),
('Content Classification', {
'fields': ('entity_type', 'cluster_role', 'taxonomy')
'fields': ('content_type', 'content_structure', 'taxonomy_term')
}),
('Planning', {
'fields': ('cluster', 'idea', 'keywords')
'fields': ('cluster', 'keywords')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
@@ -56,7 +56,7 @@ class TasksAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
class ImagesAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
list_display = ['get_content_title', 'site', 'sector', 'image_type', 'status', 'position', 'created_at']
list_filter = ['image_type', 'status', 'site', 'sector']
search_fields = ['content__title', 'content__meta_title', 'task__title']
search_fields = ['content__title']
ordering = ['-id'] # Sort by ID descending (newest first)
def get_content_title(self, obj):
@@ -86,35 +86,32 @@ class ImagesAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
@admin.register(Content)
class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
list_display = ['title', 'entity_type', 'content_format', 'cluster_role', 'site', 'sector', 'source', 'sync_status', 'word_count', 'generated_at']
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 = ['generated_at', 'updated_at']
list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'source', 'status', 'created_at']
list_filter = ['content_type', 'content_structure', 'source', 'status', 'site', 'sector', 'created_at']
search_fields = ['title', 'content_html', 'external_url']
ordering = ['-created_at']
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Basic Info', {
'fields': ('title', 'task', 'site', 'sector', 'cluster', 'status')
'fields': ('title', 'site', 'sector', 'cluster', 'status')
}),
('Content Classification', {
'fields': ('entity_type', 'content_format', 'cluster_role', 'external_type')
'fields': ('content_type', 'content_structure', 'source')
}),
('Content', {
'fields': ('html_content', 'word_count', 'json_blocks', 'structure_data')
'fields': ('content_html',)
}),
('SEO', {
'fields': ('meta_title', 'meta_description', 'primary_keyword', 'secondary_keywords')
('Taxonomy', {
'fields': ('taxonomy_terms',),
'description': 'Categories, tags, and other taxonomy terms'
}),
('WordPress Sync', {
'fields': ('source', 'sync_status', 'external_id', 'external_url', 'sync_metadata'),
'classes': ('collapse',)
}),
('Optimization', {
'fields': ('linker_version', 'optimizer_version', 'optimization_scores', 'internal_links'),
'fields': ('external_id', 'external_url'),
'classes': ('collapse',)
}),
('Timestamps', {
'fields': ('generated_at', 'updated_at'),
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
@@ -137,32 +134,23 @@ class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
@admin.register(ContentTaxonomy)
class ContentTaxonomyAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
list_display = ['name', 'taxonomy_type', 'slug', 'parent', 'external_id', 'external_taxonomy', 'sync_status', 'count', 'site', 'sector']
list_filter = ['taxonomy_type', 'sync_status', 'site', 'sector', 'parent']
search_fields = ['name', 'slug', 'description', 'external_taxonomy']
list_display = ['name', 'taxonomy_type', 'slug', 'external_id', 'external_taxonomy', 'site', 'sector']
list_filter = ['taxonomy_type', 'site', 'sector']
search_fields = ['name', 'slug', 'external_taxonomy']
ordering = ['taxonomy_type', 'name']
filter_horizontal = ['clusters']
fieldsets = (
('Basic Info', {
'fields': ('name', 'slug', 'taxonomy_type', 'description', 'site', 'sector')
}),
('Hierarchy', {
'fields': ('parent',),
'description': 'Set parent for hierarchical taxonomies (categories).'
'fields': ('name', 'slug', 'taxonomy_type', 'site', 'sector')
}),
('WordPress Sync', {
'fields': ('external_id', 'external_taxonomy', 'sync_status', 'count', 'metadata')
}),
('Semantic Mapping', {
'fields': ('clusters',),
'description': 'Map this taxonomy to semantic clusters for AI optimization.'
'fields': ('external_id', 'external_taxonomy')
}),
)
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('parent', 'site', 'sector').prefetch_related('clusters')
return qs.select_related('site', 'sector')
@admin.register(ContentAttribute)

View File

@@ -0,0 +1,348 @@
# Generated by Django 5.2.8 on 2025-11-25 15:59
import django.db.models.deletion
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'),
('writer', '0006_cleanup_migrate_and_drop_deprecated_fields'),
]
operations = [
migrations.AlterUniqueTogether(
name='contenttaxonomyrelation',
unique_together=None,
),
migrations.RemoveField(
model_name='contenttaxonomyrelation',
name='content',
),
migrations.RemoveField(
model_name='contenttaxonomyrelation',
name='taxonomy',
),
migrations.RemoveField(
model_name='content',
name='taxonomies',
),
migrations.AlterModelOptions(
name='content',
options={'ordering': ['-created_at'], 'verbose_name': 'Content', 'verbose_name_plural': 'Contents'},
),
migrations.RemoveIndex(
model_name='content',
name='igny8_conte_task_id_712988_idx',
),
migrations.RemoveIndex(
model_name='content',
name='igny8_conte_generat_7128df_idx',
),
migrations.RemoveIndex(
model_name='content',
name='igny8_conte_sync_st_02d5bd_idx',
),
migrations.RemoveIndex(
model_name='content',
name='igny8_conte_source_df78d0_idx',
),
migrations.RemoveIndex(
model_name='content',
name='igny8_conte_entity__f559b3_idx',
),
migrations.RemoveIndex(
model_name='content',
name='igny8_conte_content_b538ee_idx',
),
migrations.RemoveIndex(
model_name='content',
name='igny8_conte_cluster_32e22a_idx',
),
migrations.RemoveIndex(
model_name='content',
name='igny8_conte_externa_a26125_idx',
),
migrations.RemoveIndex(
model_name='content',
name='igny8_conte_site_id_e559d5_idx',
),
migrations.RemoveIndex(
model_name='contenttaxonomy',
name='igny8_conte_sync_st_307b43_idx',
),
migrations.RemoveIndex(
model_name='tasks',
name='igny8_tasks_entity__1dc185_idx',
),
migrations.RemoveIndex(
model_name='tasks',
name='igny8_tasks_cluster_c87903_idx',
),
migrations.RemoveField(
model_name='content',
name='cluster_role',
),
migrations.RemoveField(
model_name='content',
name='content_format',
),
migrations.RemoveField(
model_name='content',
name='entity_type',
),
migrations.RemoveField(
model_name='content',
name='external_type',
),
migrations.RemoveField(
model_name='content',
name='generated_at',
),
migrations.RemoveField(
model_name='content',
name='html_content',
),
migrations.RemoveField(
model_name='content',
name='internal_links',
),
migrations.RemoveField(
model_name='content',
name='json_blocks',
),
migrations.RemoveField(
model_name='content',
name='linker_version',
),
migrations.RemoveField(
model_name='content',
name='meta_description',
),
migrations.RemoveField(
model_name='content',
name='meta_title',
),
migrations.RemoveField(
model_name='content',
name='metadata',
),
migrations.RemoveField(
model_name='content',
name='optimization_scores',
),
migrations.RemoveField(
model_name='content',
name='optimizer_version',
),
migrations.RemoveField(
model_name='content',
name='primary_keyword',
),
migrations.RemoveField(
model_name='content',
name='secondary_keywords',
),
migrations.RemoveField(
model_name='content',
name='structure_data',
),
migrations.RemoveField(
model_name='content',
name='sync_metadata',
),
migrations.RemoveField(
model_name='content',
name='sync_status',
),
migrations.RemoveField(
model_name='content',
name='task',
),
migrations.RemoveField(
model_name='content',
name='word_count',
),
migrations.RemoveField(
model_name='contenttaxonomy',
name='clusters',
),
migrations.RemoveField(
model_name='contenttaxonomy',
name='count',
),
migrations.RemoveField(
model_name='contenttaxonomy',
name='description',
),
migrations.RemoveField(
model_name='contenttaxonomy',
name='metadata',
),
migrations.RemoveField(
model_name='contenttaxonomy',
name='parent',
),
migrations.RemoveField(
model_name='contenttaxonomy',
name='sync_status',
),
migrations.RemoveField(
model_name='tasks',
name='cluster_role',
),
migrations.RemoveField(
model_name='tasks',
name='entity_type',
),
migrations.RemoveField(
model_name='tasks',
name='idea',
),
migrations.RemoveField(
model_name='tasks',
name='keyword_objects',
),
migrations.RemoveField(
model_name='tasks',
name='taxonomy',
),
migrations.AddField(
model_name='content',
name='content_html',
field=models.TextField(default='', help_text='Final HTML content'),
preserve_default=False,
),
migrations.AddField(
model_name='content',
name='content_structure',
field=models.CharField(db_index=True, default='post', help_text='Content structure/format: article, listicle, guide, comparison, product_page, etc.', max_length=100),
preserve_default=False,
),
migrations.AddField(
model_name='content',
name='content_type',
field=models.CharField(db_index=True, default='article', help_text='Content type: post, page, product, service, category, tag, etc.', max_length=100),
preserve_default=False,
),
migrations.AddField(
model_name='content',
name='taxonomy_terms',
field=models.ManyToManyField(blank=True, db_table='igny8_content_taxonomy_relations', help_text='Associated taxonomy terms (categories, tags, attributes)', related_name='contents', to='writer.contenttaxonomy'),
),
migrations.AddField(
model_name='tasks',
name='content_structure',
field=models.CharField(db_index=True, default='', help_text='Content structure/format: article, listicle, guide, comparison, product_page, etc.', max_length=100),
preserve_default=False,
),
migrations.AddField(
model_name='tasks',
name='content_type',
field=models.CharField(db_index=True, default='post', help_text='Content type: post, page, product, service, category, tag, etc.', max_length=100),
preserve_default=False,
),
migrations.AddField(
model_name='tasks',
name='taxonomy_term',
field=models.ForeignKey(blank=True, help_text='Optional taxonomy term assignment', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to='writer.contenttaxonomy'),
),
migrations.AlterField(
model_name='content',
name='cluster',
field=models.ForeignKey(help_text='Parent cluster (required)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contents', to='planner.clusters'),
),
migrations.AlterField(
model_name='content',
name='created_at',
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name='content',
name='external_id',
field=models.CharField(blank=True, db_index=True, help_text='WordPress/external platform post ID', max_length=255, null=True),
),
migrations.AlterField(
model_name='content',
name='external_url',
field=models.URLField(blank=True, help_text='WordPress/external platform URL', null=True),
),
migrations.AlterField(
model_name='content',
name='source',
field=models.CharField(choices=[('igny8', 'IGNY8 Generated'), ('wordpress', 'WordPress Imported')], db_index=True, default='igny8', help_text='Content source', max_length=50),
),
migrations.AlterField(
model_name='content',
name='status',
field=models.CharField(choices=[('draft', 'Draft'), ('published', 'Published')], db_index=True, default='draft', help_text='Content status', max_length=50),
),
migrations.AlterField(
model_name='content',
name='title',
field=models.CharField(db_index=True, default='article', max_length=255),
preserve_default=False,
),
migrations.AlterField(
model_name='contenttaxonomy',
name='external_id',
field=models.IntegerField(blank=True, db_index=True, help_text='WordPress term_id - null for cluster taxonomies', null=True),
),
migrations.AlterField(
model_name='contenttaxonomy',
name='external_taxonomy',
field=models.CharField(blank=True, help_text='WordPress taxonomy slug (category, post_tag, product_cat, pa_*) - null for cluster taxonomies', max_length=100, null=True),
),
migrations.AlterField(
model_name='contenttaxonomy',
name='taxonomy_type',
field=models.CharField(choices=[('category', 'Category'), ('tag', 'Tag'), ('product_category', 'Product Category'), ('product_attribute', 'Product Attribute'), ('cluster', 'Cluster Taxonomy')], db_index=True, help_text='Type of taxonomy', max_length=50),
),
migrations.AlterField(
model_name='tasks',
name='cluster',
field=models.ForeignKey(help_text='Parent cluster (required)', limit_choices_to={'sector': models.F('sector')}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to='planner.clusters'),
),
migrations.RemoveField(
model_name='tasks',
name='keywords',
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['title'], name='igny8_conte_title_f13d63_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['content_type'], name='igny8_conte_content_df9458_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['content_structure'], name='igny8_conte_content_55cffb_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['status'], name='igny8_conte_status_b7cba0_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['external_id'], name='igny8_conte_externa_7ffbdf_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['site', 'sector'], name='igny8_conte_site_id_dc7938_idx'),
),
migrations.AddIndex(
model_name='tasks',
index=models.Index(fields=['content_structure'], name='igny8_tasks_content_733577_idx'),
),
migrations.DeleteModel(
name='ContentTaxonomyRelation',
),
migrations.AddField(
model_name='tasks',
name='keywords',
field=models.ManyToManyField(blank=True, help_text='Keywords linked to this task', related_name='tasks', to='planner.keywords'),
),
]

View File

@@ -5,7 +5,7 @@ from .views import (
ImagesViewSet,
ContentViewSet,
ContentTaxonomyViewSet,
ContentAttributeViewSet,
# ContentAttributeViewSet, # Disabled - serializer removed in Stage 1
)
router = DefaultRouter()
@@ -13,7 +13,7 @@ router.register(r'tasks', TasksViewSet, basename='task')
router.register(r'images', ImagesViewSet, basename='image')
router.register(r'content', ContentViewSet, basename='content')
router.register(r'taxonomies', ContentTaxonomyViewSet, basename='taxonomy')
router.register(r'attributes', ContentAttributeViewSet, basename='attribute')
# router.register(r'attributes', ContentAttributeViewSet, basename='attribute') # Disabled - serializer removed in Stage 1
urlpatterns = [
path('', include(router.urls)),

View File

@@ -16,9 +16,8 @@ from .serializers import (
ImagesSerializer,
ContentSerializer,
ContentTaxonomySerializer,
# ContentAttributeSerializer removed in Stage 1 - model no longer exists
)
from igny8_core.business.content.models import ContentTaxonomy # ContentAttribute removed in Stage 1
from igny8_core.business.content.models import ContentTaxonomy # ContentAttribute model exists but serializer removed in Stage 1
from igny8_core.business.content.services.content_generation_service import ContentGenerationService
from igny8_core.business.content.services.validation_service import ContentValidationService
from igny8_core.business.content.services.metadata_mapping_service import MetadataMappingService
@@ -544,49 +543,19 @@ class ImagesViewSet(SiteSectorModelViewSet):
except (ValueError, TypeError):
pass
# Also get content from images linked via task
task_linked_images = Images.objects.filter(task__isnull=False, content__isnull=True)
if account:
task_linked_images = task_linked_images.filter(account=account)
# Apply site/sector filtering to task-linked images
if site_id:
try:
task_linked_images = task_linked_images.filter(site_id=int(site_id))
except (ValueError, TypeError):
pass
if sector_id:
try:
task_linked_images = task_linked_images.filter(sector_id=int(sector_id))
except (ValueError, TypeError):
pass
# Get content IDs from task-linked images
task_content_ids = set()
for image in task_linked_images:
if image.task and hasattr(image.task, 'content_record'):
try:
content = image.task.content_record
if content:
task_content_ids.add(content.id)
except Exception:
pass
# Combine both sets of content IDs
content_ids = set(queryset.values_list('id', flat=True).distinct())
content_ids.update(task_content_ids)
# Task field removed in Stage 1 - images are now only linked to content directly
# All images must be linked via content, not task
# Build grouped response
grouped_data = []
content_ids = set(queryset.values_list('id', flat=True).distinct())
for content_id in content_ids:
try:
content = Content.objects.get(id=content_id)
# Get images linked directly to content OR via task
content_images = Images.objects.filter(
Q(content=content) | Q(task=content.task)
).order_by('position')
# Get images linked directly to content
content_images = Images.objects.filter(content=content).order_by('position')
# Get featured image
featured_image = content_images.filter(image_type='featured').first()
@@ -1585,82 +1554,7 @@ class ContentTaxonomyViewSet(SiteSectorModelViewSet):
)
@extend_schema_view(
list=extend_schema(tags=['Writer - Attributes']),
create=extend_schema(tags=['Writer - Attributes']),
retrieve=extend_schema(tags=['Writer - Attributes']),
update=extend_schema(tags=['Writer - Attributes']),
partial_update=extend_schema(tags=['Writer - Attributes']),
destroy=extend_schema(tags=['Writer - Attributes']),
)
class ContentAttributeViewSet(SiteSectorModelViewSet):
"""
ViewSet for managing content attributes (product specs, service modifiers, semantic facets)
Unified API Standard v1.0 compliant
"""
queryset = ContentAttribute.objects.select_related('content', 'cluster', 'site', 'sector')
serializer_class = ContentAttributeSerializer
permission_classes = [IsAuthenticatedAndActive, IsViewerOrAbove]
pagination_class = CustomPageNumberPagination
throttle_scope = 'writer'
throttle_classes = [DebugScopedRateThrottle]
# DRF filtering configuration
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
# Search configuration
search_fields = ['name', 'value', 'external_attribute_name', 'content__title']
# Ordering configuration
ordering_fields = ['name', 'attribute_type', 'created_at']
ordering = ['attribute_type', 'name']
# Filter configuration
filterset_fields = ['attribute_type', 'source', 'content', 'cluster', 'external_id']
def perform_create(self, serializer):
"""Create attribute with site/sector context"""
user = getattr(self.request, 'user', None)
try:
query_params = getattr(self.request, 'query_params', None)
if query_params is None:
query_params = getattr(self.request, 'GET', {})
except AttributeError:
query_params = {}
site_id = serializer.validated_data.get('site_id') or query_params.get('site_id')
sector_id = serializer.validated_data.get('sector_id') or query_params.get('sector_id')
from igny8_core.auth.models import Site, Sector
from rest_framework.exceptions import ValidationError
if not site_id:
raise ValidationError("site_id is required")
try:
site = Site.objects.get(id=site_id)
except Site.DoesNotExist:
raise ValidationError(f"Site with id {site_id} does not exist")
if not sector_id:
raise ValidationError("sector_id is required")
try:
sector = Sector.objects.get(id=sector_id)
if sector.site_id != site_id:
raise ValidationError(f"Sector does not belong to the selected site")
except Sector.DoesNotExist:
raise ValidationError(f"Sector with id {sector_id} does not exist")
serializer.validated_data.pop('site_id', None)
serializer.validated_data.pop('sector_id', None)
account = getattr(self.request, 'account', None)
if not account and user and user.is_authenticated and user.account:
account = user.account
if not account:
account = site.account
serializer.save(account=account, site=site, sector=sector)
# ContentAttributeViewSet temporarily disabled - ContentAttributeSerializer was removed in Stage 1
# TODO: Re-implement or remove completely based on Stage 1 architecture decisions