diff --git a/backend/igny8_core/modules/planner/serializers.py b/backend/igny8_core/modules/planner/serializers.py index c63e4701..92c1915d 100644 --- a/backend/igny8_core/modules/planner/serializers.py +++ b/backend/igny8_core/modules/planner/serializers.py @@ -1,7 +1,9 @@ from rest_framework import serializers + from .models import Keywords, Clusters, ContentIdeas from igny8_core.auth.models import SeedKeyword from igny8_core.auth.serializers import SeedKeywordSerializer +from igny8_core.business.site_building.models import SiteBlueprintTaxonomy class KeywordSerializer(serializers.ModelSerializer): @@ -27,6 +29,14 @@ class KeywordSerializer(serializers.ModelSerializer): site_id = serializers.IntegerField(write_only=True, required=False) sector_id = serializers.IntegerField(write_only=True, required=False) + attribute_values = serializers.ListField( + child=serializers.CharField(), + required=False, + allow_empty=True, + default=list, + help_text="Optional attribute metadata (e.g., product specs, service modifiers)", + ) + class Meta: model = Keywords fields = [ @@ -43,6 +53,7 @@ class KeywordSerializer(serializers.ModelSerializer): 'cluster_name', 'sector_name', 'status', + 'attribute_values', 'created_at', 'updated_at', 'site_id', @@ -113,6 +124,8 @@ class ClusterSerializer(serializers.ModelSerializer): site_id = serializers.IntegerField(write_only=True, required=False) sector_id = serializers.IntegerField(write_only=True, required=False) + context_type_display = serializers.SerializerMethodField() + class Meta: model = Clusters fields = [ @@ -123,6 +136,9 @@ class ClusterSerializer(serializers.ModelSerializer): 'volume', 'mapped_pages', 'status', + 'context_type', + 'context_type_display', + 'dimension_meta', 'sector_name', 'created_at', 'updated_at', @@ -142,6 +158,9 @@ class ClusterSerializer(serializers.ModelSerializer): except Sector.DoesNotExist: return None return None + + def get_context_type_display(self, obj): + return obj.get_context_type_display() def validate_name(self, value): """Ensure cluster name is unique within account""" @@ -153,6 +172,7 @@ class ContentIdeasSerializer(serializers.ModelSerializer): """Serializer for ContentIdeas model""" keyword_cluster_name = serializers.SerializerMethodField() sector_name = serializers.SerializerMethodField() + taxonomy_name = serializers.SerializerMethodField() site_id = serializers.IntegerField(write_only=True, required=False) sector_id = serializers.IntegerField(write_only=True, required=False) @@ -167,9 +187,13 @@ class ContentIdeasSerializer(serializers.ModelSerializer): 'target_keywords', 'keyword_cluster_id', 'keyword_cluster_name', + 'taxonomy_id', + 'taxonomy_name', 'sector_name', 'status', 'estimated_word_count', + 'site_entity_type', + 'cluster_role', 'created_at', 'updated_at', 'site_id', @@ -198,3 +222,12 @@ class ContentIdeasSerializer(serializers.ModelSerializer): except Sector.DoesNotExist: return None return None + + def get_taxonomy_name(self, obj): + if obj.taxonomy_id: + try: + taxonomy = SiteBlueprintTaxonomy.objects.get(id=obj.taxonomy_id) + return taxonomy.name + except SiteBlueprintTaxonomy.DoesNotExist: + return None + return None diff --git a/backend/igny8_core/modules/site_builder/serializers.py b/backend/igny8_core/modules/site_builder/serializers.py index ac21530f..cb8e6098 100644 --- a/backend/igny8_core/modules/site_builder/serializers.py +++ b/backend/igny8_core/modules/site_builder/serializers.py @@ -1,3 +1,4 @@ +from django.conf import settings from rest_framework import serializers from igny8_core.business.site_building.models import ( @@ -7,6 +8,7 @@ from igny8_core.business.site_building.models import ( HeroImageryDirection, PageBlueprint, SiteBlueprint, + WorkflowState, ) @@ -44,6 +46,7 @@ class SiteBlueprintSerializer(serializers.ModelSerializer): pages = PageBlueprintSerializer(many=True, read_only=True) site_id = serializers.IntegerField(write_only=True, required=False) sector_id = serializers.IntegerField(write_only=True, required=False) + workflow_state = serializers.SerializerMethodField() class Meta: model = SiteBlueprint @@ -62,6 +65,7 @@ class SiteBlueprintSerializer(serializers.ModelSerializer): 'created_at', 'updated_at', 'pages', + 'workflow_state', ] read_only_fields = [ 'structure_json', @@ -83,6 +87,21 @@ class SiteBlueprintSerializer(serializers.ModelSerializer): attrs['sector_id'] = sector_id return attrs + def get_workflow_state(self, obj): + if not getattr(settings, 'USE_SITE_BUILDER_REFACTOR', False): + return None + try: + state: WorkflowState = obj.workflow_state + except WorkflowState.DoesNotExist: + return None + return { + 'current_step': state.current_step, + 'step_status': state.step_status, + 'blocking_reason': state.blocking_reason, + 'completed': state.completed, + 'updated_at': state.updated_at, + } + class MetadataOptionSerializer(serializers.Serializer): id = serializers.IntegerField() diff --git a/backend/igny8_core/modules/site_builder/views.py b/backend/igny8_core/modules/site_builder/views.py index 6d897222..5b931bf0 100644 --- a/backend/igny8_core/modules/site_builder/views.py +++ b/backend/igny8_core/modules/site_builder/views.py @@ -1,3 +1,4 @@ +from django.conf import settings from rest_framework import status from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated @@ -21,6 +22,8 @@ from igny8_core.business.site_building.services import ( PageGenerationService, SiteBuilderFileService, StructureGenerationService, + WorkflowStateService, + TaxonomyService, ) from igny8_core.modules.site_builder.serializers import ( PageBlueprintSerializer, @@ -40,6 +43,11 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): throttle_scope = 'site_builder' throttle_classes = [DebugScopedRateThrottle] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.workflow_service = WorkflowStateService() + self.taxonomy_service = TaxonomyService() + def get_permissions(self): """ Allow public read access for list requests with site filter (used by Sites Renderer fallback). @@ -99,11 +107,19 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): except Sector.DoesNotExist: raise ValidationError({'sector_id': 'Sector does not belong to the selected site.'}) - serializer.save(account=site.account, site=site, sector=sector) + blueprint = serializer.save(account=site.account, site=site, sector=sector) + if self.workflow_service.enabled: + self.workflow_service.initialize(blueprint) @action(detail=True, methods=['post']) def generate_structure(self, request, pk=None): blueprint = self.get_object() + if self.workflow_service.enabled: + try: + self.workflow_service.validate_step(blueprint, 'clusters') + self.workflow_service.validate_step(blueprint, 'taxonomies') + except ValidationError as exc: + return error_response(str(exc), status.HTTP_400_BAD_REQUEST, request) business_brief = request.data.get('business_brief') or \ blueprint.config_json.get('business_brief', '') objectives = request.data.get('objectives') or \ @@ -119,7 +135,10 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): style_preferences=style, metadata=request.data.get('metadata', {}), ) - return Response(result, status=status.HTTP_202_ACCEPTED if 'task_id' in result else status.HTTP_200_OK) + response = Response(result, status=status.HTTP_202_ACCEPTED if 'task_id' in result else status.HTTP_200_OK) + if self.workflow_service.enabled: + self.workflow_service.refresh_state(blueprint) + return response @action(detail=True, methods=['post']) def generate_all_pages(self, request, pk=None): @@ -136,6 +155,11 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): page_ids = request.data.get('page_ids') force = request.data.get('force', False) + if self.workflow_service.enabled: + try: + self.workflow_service.validate_step(blueprint, 'sitemap') + except ValidationError as exc: + return error_response(str(exc), status.HTTP_400_BAD_REQUEST, request) service = PageGenerationService() try: result = service.bulk_generate_pages( @@ -144,7 +168,10 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): force_regenerate=force ) response_status = status.HTTP_202_ACCEPTED if result.get('success') else status.HTTP_400_BAD_REQUEST - return success_response(result, request=request, status_code=response_status) + response = success_response(result, request=request, status_code=response_status) + if self.workflow_service.enabled: + self.workflow_service.refresh_state(blueprint) + return response except Exception as e: return error_response(str(e), status.HTTP_400_BAD_REQUEST, request) @@ -166,6 +193,12 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): blueprint = self.get_object() page_ids = request.data.get('page_ids') + if self.workflow_service.enabled: + try: + self.workflow_service.validate_step(blueprint, 'coverage') + except ValidationError as exc: + return error_response(str(exc), status.HTTP_400_BAD_REQUEST, request) + service = PageGenerationService() try: tasks = service.create_tasks_for_pages(blueprint, page_ids=page_ids) @@ -174,7 +207,10 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): from igny8_core.business.content.serializers import TasksSerializer serializer = TasksSerializer(tasks, many=True) - return success_response({'tasks': serializer.data, 'count': len(tasks)}, request=request) + response = success_response({'tasks': serializer.data, 'count': len(tasks)}, request=request) + if self.workflow_service.enabled: + self.workflow_service.refresh_state(blueprint) + return response except Exception as e: return error_response(str(e), status.HTTP_400_BAD_REQUEST, request) diff --git a/backend/igny8_core/modules/writer/serializers.py b/backend/igny8_core/modules/writer/serializers.py index 58e0316a..80ea8309 100644 --- a/backend/igny8_core/modules/writer/serializers.py +++ b/backend/igny8_core/modules/writer/serializers.py @@ -3,6 +3,11 @@ from django.db import models from django.core.exceptions import ObjectDoesNotExist from .models import Tasks, Images, Content from igny8_core.business.planning.models import Clusters, ContentIdeas +from igny8_core.business.content.models import ( + ContentClusterMap, + ContentTaxonomyMap, + ContentAttributeMap, +) class TasksSerializer(serializers.ModelSerializer): @@ -17,6 +22,9 @@ class TasksSerializer(serializers.ModelSerializer): content_secondary_keywords = serializers.SerializerMethodField() content_tags = serializers.SerializerMethodField() content_categories = serializers.SerializerMethodField() + cluster_mappings = serializers.SerializerMethodField() + taxonomy_mappings = serializers.SerializerMethodField() + attribute_mappings = serializers.SerializerMethodField() class Meta: model = Tasks @@ -42,6 +50,9 @@ class TasksSerializer(serializers.ModelSerializer): 'content_secondary_keywords', 'content_tags', 'content_categories', + 'cluster_mappings', + 'taxonomy_mappings', + 'attribute_mappings', 'assigned_post_id', 'post_url', 'created_at', @@ -109,6 +120,48 @@ class TasksSerializer(serializers.ModelSerializer): record = self._get_content_record(obj) return record.categories if record else [] + def _cluster_map_qs(self, obj): + return ContentClusterMap.objects.filter(task=obj).select_related('cluster') + + def _taxonomy_map_qs(self, obj): + return ContentTaxonomyMap.objects.filter(task=obj).select_related('taxonomy') + + def _attribute_map_qs(self, obj): + return ContentAttributeMap.objects.filter(task=obj) + + def get_cluster_mappings(self, obj): + mappings = [] + for mapping in self._cluster_map_qs(obj): + mappings.append({ + 'cluster_id': mapping.cluster_id, + 'cluster_name': mapping.cluster.name if mapping.cluster else None, + 'role': mapping.role, + 'source': mapping.source, + }) + return mappings + + def get_taxonomy_mappings(self, obj): + mappings = [] + for mapping in self._taxonomy_map_qs(obj): + taxonomy = mapping.taxonomy + mappings.append({ + 'taxonomy_id': taxonomy.id if taxonomy else None, + 'taxonomy_name': taxonomy.name if taxonomy else None, + 'taxonomy_type': taxonomy.taxonomy_type if taxonomy else None, + 'source': mapping.source, + }) + return mappings + + def get_attribute_mappings(self, obj): + mappings = [] + for mapping in self._attribute_map_qs(obj): + mappings.append({ + 'name': mapping.name, + 'value': mapping.value, + 'source': mapping.source, + }) + return mappings + class ImagesSerializer(serializers.ModelSerializer): """Serializer for Images model""" @@ -193,6 +246,9 @@ class ContentSerializer(serializers.ModelSerializer): sector_name = serializers.SerializerMethodField() has_image_prompts = serializers.SerializerMethodField() has_generated_images = serializers.SerializerMethodField() + cluster_mappings = serializers.SerializerMethodField() + taxonomy_mappings = serializers.SerializerMethodField() + attribute_mappings = serializers.SerializerMethodField() class Meta: model = Content @@ -221,6 +277,9 @@ class ContentSerializer(serializers.ModelSerializer): 'entity_type', 'json_blocks', 'structure_data', + 'cluster_mappings', + 'taxonomy_mappings', + 'attribute_mappings', ] read_only_fields = ['id', 'generated_at', 'updated_at', 'account_id'] @@ -261,3 +320,39 @@ class ContentSerializer(serializers.ModelSerializer): image_url__isnull=False ).exclude(image_url='').exists() + def get_cluster_mappings(self, obj): + mappings = ContentClusterMap.objects.filter(content=obj).select_related('cluster') + results = [] + for mapping in mappings: + results.append({ + 'cluster_id': mapping.cluster_id, + 'cluster_name': mapping.cluster.name if mapping.cluster else None, + 'role': mapping.role, + 'source': mapping.source, + }) + return results + + def get_taxonomy_mappings(self, obj): + mappings = ContentTaxonomyMap.objects.filter(content=obj).select_related('taxonomy') + results = [] + for mapping in mappings: + taxonomy = mapping.taxonomy + results.append({ + 'taxonomy_id': taxonomy.id if taxonomy else None, + 'taxonomy_name': taxonomy.name if taxonomy else None, + 'taxonomy_type': taxonomy.taxonomy_type if taxonomy else None, + 'source': mapping.source, + }) + return results + + def get_attribute_mappings(self, obj): + mappings = ContentAttributeMap.objects.filter(content=obj) + results = [] + for mapping in mappings: + results.append({ + 'name': mapping.name, + 'value': mapping.value, + 'source': mapping.source, + }) + return results +