stage1 part b
This commit is contained in:
@@ -1,31 +1,18 @@
|
||||
from rest_framework import serializers
|
||||
from django.db import models
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.conf import settings
|
||||
from .models import Tasks, Images, Content
|
||||
from igny8_core.business.planning.models import Clusters, ContentIdeas
|
||||
from igny8_core.business.content.models import (
|
||||
ContentClusterMap,
|
||||
ContentTaxonomyMap,
|
||||
ContentAttribute,
|
||||
ContentTaxonomy,
|
||||
ContentTaxonomyRelation,
|
||||
)
|
||||
# Backward compatibility
|
||||
ContentAttributeMap = ContentAttribute
|
||||
from igny8_core.business.planning.models import Clusters
|
||||
from igny8_core.business.content.models import ContentTaxonomy
|
||||
|
||||
|
||||
class TasksSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for Tasks model"""
|
||||
"""Serializer for Tasks model - Stage 1 refactored"""
|
||||
cluster_name = serializers.SerializerMethodField()
|
||||
sector_name = serializers.SerializerMethodField()
|
||||
idea_title = serializers.SerializerMethodField()
|
||||
site_id = serializers.IntegerField(write_only=True, required=False)
|
||||
sector_id = serializers.IntegerField(write_only=True, required=False)
|
||||
content_html = serializers.SerializerMethodField()
|
||||
content_primary_keyword = serializers.SerializerMethodField()
|
||||
content_secondary_keywords = serializers.SerializerMethodField()
|
||||
# tags/categories removed — use taxonomies M2M on Content
|
||||
|
||||
class Meta:
|
||||
model = Tasks
|
||||
@@ -33,17 +20,13 @@ class TasksSerializer(serializers.ModelSerializer):
|
||||
'id',
|
||||
'title',
|
||||
'description',
|
||||
'keywords',
|
||||
'cluster_id',
|
||||
'cluster_name',
|
||||
'sector_name',
|
||||
'idea_id',
|
||||
'idea_title',
|
||||
'content_type',
|
||||
'content_structure',
|
||||
'taxonomy_term_id',
|
||||
'status',
|
||||
# task-level raw content/seo fields removed — stored on Content
|
||||
'content_html',
|
||||
'content_primary_keyword',
|
||||
'content_secondary_keywords',
|
||||
'sector_name',
|
||||
'site_id',
|
||||
'sector_id',
|
||||
'account_id',
|
||||
@@ -52,13 +35,19 @@ class TasksSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Only include Stage 1 fields when feature flag is enabled
|
||||
if getattr(settings, 'USE_SITE_BUILDER_REFACTOR', False):
|
||||
self.fields['cluster_mappings'] = serializers.SerializerMethodField()
|
||||
self.fields['taxonomy_mappings'] = serializers.SerializerMethodField()
|
||||
self.fields['attribute_mappings'] = serializers.SerializerMethodField()
|
||||
def validate(self, attrs):
|
||||
"""Ensure required fields for Task creation"""
|
||||
if self.instance is None: # Create operation
|
||||
if not attrs.get('cluster_id') and not attrs.get('cluster'):
|
||||
raise ValidationError({'cluster': 'Cluster is required'})
|
||||
if not attrs.get('content_type'):
|
||||
raise ValidationError({'content_type': 'Content type is required'})
|
||||
if not attrs.get('content_structure'):
|
||||
raise ValidationError({'content_structure': 'Content structure is required'})
|
||||
# Default status to queued if not provided
|
||||
if 'status' not in attrs:
|
||||
attrs['status'] = 'queued'
|
||||
return attrs
|
||||
|
||||
def get_cluster_name(self, obj):
|
||||
"""Get cluster name from Clusters model"""
|
||||
@@ -80,90 +69,6 @@ class TasksSerializer(serializers.ModelSerializer):
|
||||
except Sector.DoesNotExist:
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_idea_title(self, obj):
|
||||
"""Get idea title from ContentIdeas model"""
|
||||
if obj.idea_id:
|
||||
try:
|
||||
idea = ContentIdeas.objects.get(id=obj.idea_id)
|
||||
return idea.idea_title
|
||||
except ContentIdeas.DoesNotExist:
|
||||
return None
|
||||
return None
|
||||
|
||||
def _get_content_record(self, obj):
|
||||
try:
|
||||
return obj.content_record
|
||||
except (AttributeError, ObjectDoesNotExist):
|
||||
return None
|
||||
|
||||
def get_content_html(self, obj):
|
||||
record = self._get_content_record(obj)
|
||||
return record.html_content if record else None
|
||||
|
||||
def get_content_primary_keyword(self, obj):
|
||||
record = self._get_content_record(obj)
|
||||
return record.primary_keyword if record else None
|
||||
|
||||
def get_content_secondary_keywords(self, obj):
|
||||
record = self._get_content_record(obj)
|
||||
return record.secondary_keywords if record else []
|
||||
|
||||
def get_content_tags(self, obj):
|
||||
# tags removed; derive taxonomies from Content.taxonomies if needed
|
||||
record = self._get_content_record(obj)
|
||||
if not record:
|
||||
return []
|
||||
return [t.name for t in record.taxonomies.all()]
|
||||
|
||||
def get_content_categories(self, obj):
|
||||
# categories removed; derive hierarchical taxonomies from Content.taxonomies
|
||||
record = self._get_content_record(obj)
|
||||
if not record:
|
||||
return []
|
||||
return [t.name for t in record.taxonomies.filter(taxonomy_type__in=['category','product_cat'])]
|
||||
|
||||
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):
|
||||
@@ -244,60 +149,68 @@ class ContentImagesGroupSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class ContentSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for Content model"""
|
||||
task_title = serializers.SerializerMethodField()
|
||||
"""Serializer for Content model - Stage 1 refactored"""
|
||||
cluster_name = serializers.SerializerMethodField()
|
||||
sector_name = serializers.SerializerMethodField()
|
||||
has_image_prompts = serializers.SerializerMethodField()
|
||||
has_generated_images = serializers.SerializerMethodField()
|
||||
taxonomy_terms_data = serializers.SerializerMethodField()
|
||||
site_id = serializers.IntegerField(write_only=True, required=False)
|
||||
sector_id = serializers.IntegerField(write_only=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Content
|
||||
fields = [
|
||||
'id',
|
||||
'task_id',
|
||||
'task_title',
|
||||
'sector_name',
|
||||
'html_content',
|
||||
'word_count',
|
||||
'metadata',
|
||||
'title',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
'primary_keyword',
|
||||
'secondary_keywords',
|
||||
'content_html',
|
||||
'cluster_id',
|
||||
'cluster_name',
|
||||
'content_type',
|
||||
'content_structure',
|
||||
'taxonomy_terms_data',
|
||||
'external_id',
|
||||
'external_url',
|
||||
'source',
|
||||
'status',
|
||||
'generated_at',
|
||||
'updated_at',
|
||||
'sector_name',
|
||||
'site_id',
|
||||
'sector_id',
|
||||
'account_id',
|
||||
'has_image_prompts',
|
||||
'has_generated_images',
|
||||
# Phase 8: Universal Content Types
|
||||
'entity_type',
|
||||
'json_blocks',
|
||||
'structure_data',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
]
|
||||
read_only_fields = ['id', 'generated_at', 'updated_at', 'account_id']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Only include Stage 1 fields when feature flag is enabled
|
||||
if getattr(settings, 'USE_SITE_BUILDER_REFACTOR', False):
|
||||
self.fields['cluster_mappings'] = serializers.SerializerMethodField()
|
||||
self.fields['taxonomy_mappings'] = serializers.SerializerMethodField()
|
||||
self.fields['attribute_mappings'] = serializers.SerializerMethodField()
|
||||
def validate(self, attrs):
|
||||
"""Ensure required fields for Content creation"""
|
||||
if self.instance is None: # Create operation
|
||||
if not attrs.get('cluster_id') and not attrs.get('cluster'):
|
||||
raise ValidationError({'cluster': 'Cluster is required'})
|
||||
if not attrs.get('content_type'):
|
||||
raise ValidationError({'content_type': 'Content type is required'})
|
||||
if not attrs.get('content_structure'):
|
||||
raise ValidationError({'content_structure': 'Content structure is required'})
|
||||
if not attrs.get('title'):
|
||||
raise ValidationError({'title': 'Title is required'})
|
||||
# Default source to igny8 if not provided
|
||||
if 'source' not in attrs:
|
||||
attrs['source'] = 'igny8'
|
||||
# Default status to draft if not provided
|
||||
if 'status' not in attrs:
|
||||
attrs['status'] = 'draft'
|
||||
return attrs
|
||||
|
||||
def get_task_title(self, obj):
|
||||
"""Get task title"""
|
||||
if obj.task_id:
|
||||
def get_cluster_name(self, obj):
|
||||
"""Get cluster name"""
|
||||
if obj.cluster_id:
|
||||
try:
|
||||
task = Tasks.objects.get(id=obj.task_id)
|
||||
return task.title
|
||||
except Tasks.DoesNotExist:
|
||||
cluster = Clusters.objects.get(id=obj.cluster_id)
|
||||
return cluster.name
|
||||
except Clusters.DoesNotExist:
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_sector_name(self, obj):
|
||||
"""Get sector name from Sector model"""
|
||||
"""Get sector name"""
|
||||
if obj.sector_id:
|
||||
try:
|
||||
from igny8_core.auth.models import Sector
|
||||
@@ -307,66 +220,26 @@ class ContentSerializer(serializers.ModelSerializer):
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_has_image_prompts(self, obj):
|
||||
"""Check if content has any image prompts generated"""
|
||||
# Check if any images exist with prompts for this content
|
||||
return Images.objects.filter(
|
||||
models.Q(content=obj) | models.Q(task=obj.task)
|
||||
).exclude(prompt__isnull=True).exclude(prompt='').exists()
|
||||
|
||||
def get_has_generated_images(self, obj):
|
||||
"""Check if content has any generated images (status='generated' and has URL)"""
|
||||
# Check if any images are generated (have status='generated' and image_url)
|
||||
return Images.objects.filter(
|
||||
models.Q(content=obj) | models.Q(task=obj.task),
|
||||
status='generated',
|
||||
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 = ContentAttribute.objects.filter(content=obj)
|
||||
results = []
|
||||
for mapping in mappings:
|
||||
results.append({
|
||||
'name': mapping.name,
|
||||
'value': mapping.value,
|
||||
'attribute_type': mapping.attribute_type,
|
||||
'source': mapping.source,
|
||||
'external_id': mapping.external_id,
|
||||
})
|
||||
return results
|
||||
def get_taxonomy_terms_data(self, obj):
|
||||
"""Get taxonomy terms with details"""
|
||||
return [
|
||||
{
|
||||
'id': term.id,
|
||||
'name': term.name,
|
||||
'slug': term.slug,
|
||||
'taxonomy_type': term.taxonomy_type,
|
||||
'external_id': term.external_id,
|
||||
'external_taxonomy': term.external_taxonomy,
|
||||
}
|
||||
for term in obj.taxonomy_terms.all()
|
||||
]
|
||||
|
||||
|
||||
class ContentTaxonomySerializer(serializers.ModelSerializer):
|
||||
"""Serializer for ContentTaxonomy model"""
|
||||
parent_name = serializers.SerializerMethodField()
|
||||
cluster_names = serializers.SerializerMethodField()
|
||||
"""Serializer for ContentTaxonomy model - Stage 1 refactored"""
|
||||
content_count = serializers.SerializerMethodField()
|
||||
site_id = serializers.IntegerField(write_only=True, required=False)
|
||||
sector_id = serializers.IntegerField(write_only=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = ContentTaxonomy
|
||||
@@ -375,15 +248,8 @@ class ContentTaxonomySerializer(serializers.ModelSerializer):
|
||||
'name',
|
||||
'slug',
|
||||
'taxonomy_type',
|
||||
'description',
|
||||
'parent',
|
||||
'parent_name',
|
||||
'external_id',
|
||||
'external_taxonomy',
|
||||
'sync_status',
|
||||
'count',
|
||||
'metadata',
|
||||
'cluster_names',
|
||||
'content_count',
|
||||
'site_id',
|
||||
'sector_id',
|
||||
@@ -393,136 +259,12 @@ class ContentTaxonomySerializer(serializers.ModelSerializer):
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id']
|
||||
|
||||
def get_parent_name(self, obj):
|
||||
return obj.parent.name if obj.parent else None
|
||||
|
||||
def get_cluster_names(self, obj):
|
||||
return [cluster.name for cluster in obj.clusters.all()]
|
||||
|
||||
def get_content_count(self, obj):
|
||||
"""Get count of content using this taxonomy"""
|
||||
return obj.contents.count()
|
||||
|
||||
|
||||
class ContentAttributeSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for ContentAttribute model"""
|
||||
content_title = serializers.SerializerMethodField()
|
||||
cluster_name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ContentAttribute
|
||||
fields = [
|
||||
'id',
|
||||
'content',
|
||||
'content_title',
|
||||
'cluster',
|
||||
'cluster_name',
|
||||
'attribute_type',
|
||||
'name',
|
||||
'value',
|
||||
'external_id',
|
||||
'external_attribute_name',
|
||||
'source',
|
||||
'metadata',
|
||||
'site_id',
|
||||
'sector_id',
|
||||
'account_id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id']
|
||||
|
||||
def get_content_title(self, obj):
|
||||
return obj.content.title if obj.content else None
|
||||
|
||||
def get_cluster_name(self, obj):
|
||||
return obj.cluster.name if obj.cluster else None
|
||||
|
||||
|
||||
class ContentTaxonomyRelationSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for ContentTaxonomyRelation (M2M through model)"""
|
||||
content_title = serializers.SerializerMethodField()
|
||||
taxonomy_name = serializers.SerializerMethodField()
|
||||
taxonomy_type = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ContentTaxonomyRelation
|
||||
fields = [
|
||||
'id',
|
||||
'content',
|
||||
'content_title',
|
||||
'taxonomy',
|
||||
'taxonomy_name',
|
||||
'taxonomy_type',
|
||||
'created_at',
|
||||
]
|
||||
read_only_fields = ['id', 'created_at']
|
||||
|
||||
def get_content_title(self, obj):
|
||||
return obj.content.title if obj.content else None
|
||||
|
||||
def get_taxonomy_name(self, obj):
|
||||
return obj.taxonomy.name if obj.taxonomy else None
|
||||
|
||||
def get_taxonomy_type(self, obj):
|
||||
return obj.taxonomy.taxonomy_type if obj.taxonomy else None
|
||||
|
||||
|
||||
class UpdatedTasksSerializer(serializers.ModelSerializer):
|
||||
"""Updated Serializer for Tasks model with new fields."""
|
||||
cluster_name = serializers.SerializerMethodField()
|
||||
sector_name = serializers.SerializerMethodField()
|
||||
idea_title = serializers.SerializerMethodField()
|
||||
site_id = serializers.IntegerField(write_only=True, required=False)
|
||||
sector_id = serializers.IntegerField(write_only=True, required=False)
|
||||
content_html = serializers.SerializerMethodField()
|
||||
content_primary_keyword = serializers.SerializerMethodField()
|
||||
content_secondary_keywords = serializers.SerializerMethodField()
|
||||
content_taxonomies = serializers.SerializerMethodField()
|
||||
content_attributes = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Tasks
|
||||
fields = [
|
||||
'id',
|
||||
'title',
|
||||
'description',
|
||||
'keywords',
|
||||
'cluster_id',
|
||||
'cluster_name',
|
||||
'sector_name',
|
||||
'idea_id',
|
||||
'idea_title',
|
||||
'content_structure',
|
||||
'content_type',
|
||||
'status',
|
||||
'content',
|
||||
'word_count',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
'content_html',
|
||||
'content_primary_keyword',
|
||||
'content_secondary_keywords',
|
||||
'content_taxonomies',
|
||||
'content_attributes',
|
||||
'assigned_post_id',
|
||||
'post_url',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'site_id',
|
||||
'sector_id',
|
||||
'account_id',
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id']
|
||||
|
||||
def get_content_taxonomies(self, obj):
|
||||
"""Fetch related taxonomies."""
|
||||
return ContentTaxonomyRelationSerializer(
|
||||
obj.content.taxonomies.all(), many=True
|
||||
).data
|
||||
|
||||
def get_content_attributes(self, obj):
|
||||
"""Fetch related attributes."""
|
||||
return ContentAttributeSerializer(
|
||||
obj.content.attributes.all(), many=True
|
||||
).data
|
||||
# ContentAttributeSerializer and ContentTaxonomyRelationSerializer removed in Stage 1
|
||||
# These models no longer exist - simplified to direct M2M relationships
|
||||
|
||||
# UpdatedTasksSerializer removed - duplicate of TasksSerializer which is already refactored
|
||||
|
||||
181
backend/igny8_core/modules/writer/tests/test_stage1_refactor.py
Normal file
181
backend/igny8_core/modules/writer/tests/test_stage1_refactor.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
Stage 1 Backend Refactor - Basic Tests
|
||||
Test the refactored models and serializers
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.test import TestCase
|
||||
from igny8_core.business.planning.models import Clusters
|
||||
from igny8_core.business.content.models import Tasks, Content, ContentTaxonomy
|
||||
from igny8_core.modules.writer.serializers import TasksSerializer, ContentSerializer, ContentTaxonomySerializer
|
||||
|
||||
|
||||
class TestClusterModel(TestCase):
|
||||
"""Test Cluster model after Stage 1 refactor"""
|
||||
|
||||
def test_cluster_fields_removed(self):
|
||||
"""Verify deprecated fields are removed"""
|
||||
cluster = Clusters()
|
||||
|
||||
# These fields should NOT exist
|
||||
assert not hasattr(cluster, 'context_type'), "context_type should be removed"
|
||||
assert not hasattr(cluster, 'dimension_meta'), "dimension_meta should be removed"
|
||||
|
||||
# These fields SHOULD exist
|
||||
assert hasattr(cluster, 'name'), "name field should exist"
|
||||
assert hasattr(cluster, 'keywords'), "keywords field should exist"
|
||||
|
||||
|
||||
class TestTasksModel(TestCase):
|
||||
"""Test Tasks model after Stage 1 refactor"""
|
||||
|
||||
def test_tasks_fields_removed(self):
|
||||
"""Verify deprecated fields are removed"""
|
||||
task = Tasks()
|
||||
|
||||
# These fields should NOT exist
|
||||
assert not hasattr(task, 'cluster_role'), "cluster_role should be removed"
|
||||
assert not hasattr(task, 'idea_id'), "idea_id should be removed"
|
||||
assert not hasattr(task, 'content_record'), "content_record should be removed"
|
||||
assert not hasattr(task, 'entity_type'), "entity_type should be removed"
|
||||
|
||||
def test_tasks_fields_added(self):
|
||||
"""Verify new fields are added"""
|
||||
task = Tasks()
|
||||
|
||||
# These fields SHOULD exist
|
||||
assert hasattr(task, 'content_type'), "content_type should be added"
|
||||
assert hasattr(task, 'content_structure'), "content_structure should be added"
|
||||
assert hasattr(task, 'taxonomy_term_id'), "taxonomy_term_id should be added"
|
||||
|
||||
def test_tasks_status_choices(self):
|
||||
"""Verify status choices are simplified"""
|
||||
# Status should only have 'queued' and 'completed'
|
||||
status_choices = [choice[0] for choice in Tasks._meta.get_field('status').choices]
|
||||
assert 'queued' in status_choices, "queued should be a valid status"
|
||||
assert 'completed' in status_choices, "completed should be a valid status"
|
||||
assert len(status_choices) == 2, "Should only have 2 status choices"
|
||||
|
||||
|
||||
class TestContentModel(TestCase):
|
||||
"""Test Content model after Stage 1 refactor"""
|
||||
|
||||
def test_content_fields_removed(self):
|
||||
"""Verify deprecated fields are removed"""
|
||||
content = Content()
|
||||
|
||||
# These fields should NOT exist
|
||||
assert not hasattr(content, 'task'), "task FK should be removed"
|
||||
assert not hasattr(content, 'html_content'), "html_content should be removed (use content_html)"
|
||||
assert not hasattr(content, 'entity_type'), "entity_type should be removed"
|
||||
assert not hasattr(content, 'cluster_role'), "cluster_role should be removed"
|
||||
assert not hasattr(content, 'sync_status'), "sync_status should be removed"
|
||||
|
||||
def test_content_fields_added(self):
|
||||
"""Verify new fields are added"""
|
||||
content = Content()
|
||||
|
||||
# These fields SHOULD exist
|
||||
assert hasattr(content, 'title'), "title should be added"
|
||||
assert hasattr(content, 'content_html'), "content_html should be added"
|
||||
assert hasattr(content, 'cluster_id'), "cluster_id should be added"
|
||||
assert hasattr(content, 'content_type'), "content_type should be added"
|
||||
assert hasattr(content, 'content_structure'), "content_structure should be added"
|
||||
assert hasattr(content, 'taxonomy_terms'), "taxonomy_terms M2M should exist"
|
||||
|
||||
def test_content_status_choices(self):
|
||||
"""Verify status choices are simplified"""
|
||||
# Status should only have 'draft' and 'published'
|
||||
status_choices = [choice[0] for choice in Content._meta.get_field('status').choices]
|
||||
assert 'draft' in status_choices, "draft should be a valid status"
|
||||
assert 'published' in status_choices, "published should be a valid status"
|
||||
assert len(status_choices) == 2, "Should only have 2 status choices"
|
||||
|
||||
|
||||
class TestContentTaxonomyModel(TestCase):
|
||||
"""Test ContentTaxonomy model after Stage 1 refactor"""
|
||||
|
||||
def test_taxonomy_fields_removed(self):
|
||||
"""Verify deprecated fields are removed"""
|
||||
taxonomy = ContentTaxonomy()
|
||||
|
||||
# These fields should NOT exist
|
||||
assert not hasattr(taxonomy, 'description'), "description should be removed"
|
||||
assert not hasattr(taxonomy, 'parent'), "parent FK should be removed"
|
||||
assert not hasattr(taxonomy, 'sync_status'), "sync_status should be removed"
|
||||
assert not hasattr(taxonomy, 'count'), "count should be removed"
|
||||
assert not hasattr(taxonomy, 'metadata'), "metadata should be removed"
|
||||
assert not hasattr(taxonomy, 'clusters'), "clusters M2M should be removed"
|
||||
|
||||
def test_taxonomy_type_includes_cluster(self):
|
||||
"""Verify taxonomy_type includes 'cluster' option"""
|
||||
type_choices = [choice[0] for choice in ContentTaxonomy._meta.get_field('taxonomy_type').choices]
|
||||
assert 'category' in type_choices, "category should be a valid type"
|
||||
assert 'post_tag' in type_choices, "post_tag should be a valid type"
|
||||
assert 'cluster' in type_choices, "cluster should be a valid type"
|
||||
|
||||
|
||||
class TestTasksSerializer(TestCase):
|
||||
"""Test TasksSerializer after Stage 1 refactor"""
|
||||
|
||||
def test_serializer_fields(self):
|
||||
"""Verify serializer has correct fields"""
|
||||
serializer = TasksSerializer()
|
||||
fields = serializer.fields.keys()
|
||||
|
||||
# Should have new fields
|
||||
assert 'content_type' in fields, "content_type should be in serializer"
|
||||
assert 'content_structure' in fields, "content_structure should be in serializer"
|
||||
assert 'taxonomy_term_id' in fields, "taxonomy_term_id should be in serializer"
|
||||
assert 'cluster_id' in fields, "cluster_id should be in serializer"
|
||||
|
||||
# Should NOT have deprecated fields
|
||||
assert 'idea_title' not in fields, "idea_title should not be in serializer"
|
||||
assert 'cluster_role' not in fields, "cluster_role should not be in serializer"
|
||||
|
||||
|
||||
class TestContentSerializer(TestCase):
|
||||
"""Test ContentSerializer after Stage 1 refactor"""
|
||||
|
||||
def test_serializer_fields(self):
|
||||
"""Verify serializer has correct fields"""
|
||||
serializer = ContentSerializer()
|
||||
fields = serializer.fields.keys()
|
||||
|
||||
# Should have new fields
|
||||
assert 'title' in fields, "title should be in serializer"
|
||||
assert 'content_html' in fields, "content_html should be in serializer"
|
||||
assert 'cluster_id' in fields, "cluster_id should be in serializer"
|
||||
assert 'content_type' in fields, "content_type should be in serializer"
|
||||
assert 'content_structure' in fields, "content_structure should be in serializer"
|
||||
assert 'taxonomy_terms_data' in fields, "taxonomy_terms_data should be in serializer"
|
||||
|
||||
# Should NOT have deprecated fields
|
||||
assert 'task_id' not in fields, "task_id should not be in serializer"
|
||||
assert 'entity_type' not in fields, "entity_type should not be in serializer"
|
||||
assert 'cluster_role' not in fields, "cluster_role should not be in serializer"
|
||||
|
||||
|
||||
class TestContentTaxonomySerializer(TestCase):
|
||||
"""Test ContentTaxonomySerializer after Stage 1 refactor"""
|
||||
|
||||
def test_serializer_fields(self):
|
||||
"""Verify serializer has correct fields"""
|
||||
serializer = ContentTaxonomySerializer()
|
||||
fields = serializer.fields.keys()
|
||||
|
||||
# Should have these fields
|
||||
assert 'id' in fields
|
||||
assert 'name' in fields
|
||||
assert 'slug' in fields
|
||||
assert 'taxonomy_type' in fields
|
||||
|
||||
# Should NOT have deprecated fields
|
||||
assert 'description' not in fields, "description should not be in serializer"
|
||||
assert 'parent' not in fields, "parent should not be in serializer"
|
||||
assert 'sync_status' not in fields, "sync_status should not be in serializer"
|
||||
assert 'cluster_names' not in fields, "cluster_names should not be in serializer"
|
||||
|
||||
|
||||
# Run tests with: python manage.py test igny8_core.modules.writer.tests.test_stage1_refactor
|
||||
# Or with pytest: pytest backend/igny8_core/modules/writer/tests/test_stage1_refactor.py -v
|
||||
@@ -16,15 +16,16 @@ from .serializers import (
|
||||
ImagesSerializer,
|
||||
ContentSerializer,
|
||||
ContentTaxonomySerializer,
|
||||
ContentAttributeSerializer,
|
||||
# ContentAttributeSerializer removed in Stage 1 - model no longer exists
|
||||
)
|
||||
from igny8_core.business.content.models import ContentTaxonomy, ContentAttribute
|
||||
from igny8_core.business.content.models import ContentTaxonomy # ContentAttribute 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
|
||||
from igny8_core.business.billing.exceptions import InsufficientCreditsError
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(tags=['Writer']),
|
||||
create=extend_schema(tags=['Writer']),
|
||||
@@ -37,8 +38,9 @@ class TasksViewSet(SiteSectorModelViewSet):
|
||||
"""
|
||||
ViewSet for managing tasks with CRUD operations
|
||||
Unified API Standard v1.0 compliant
|
||||
Stage 1 Refactored - removed deprecated filters
|
||||
"""
|
||||
queryset = Tasks.objects.select_related('content_record')
|
||||
queryset = Tasks.objects.select_related('cluster', 'site', 'sector')
|
||||
serializer_class = TasksSerializer
|
||||
permission_classes = [IsAuthenticatedAndActive, IsViewerOrAbove]
|
||||
pagination_class = CustomPageNumberPagination # Explicitly use custom pagination
|
||||
@@ -55,8 +57,8 @@ class TasksViewSet(SiteSectorModelViewSet):
|
||||
ordering_fields = ['title', 'created_at', 'status']
|
||||
ordering = ['-created_at'] # Default ordering (newest first)
|
||||
|
||||
# Filter configuration
|
||||
filterset_fields = ['status', 'entity_type', 'cluster_role', 'cluster_id']
|
||||
# Filter configuration - Stage 1: removed entity_type, cluster_role
|
||||
filterset_fields = ['status', 'cluster_id', 'content_type', 'content_structure']
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Require explicit site_id and sector_id - no defaults."""
|
||||
@@ -757,8 +759,9 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
"""
|
||||
ViewSet for managing content with new unified structure
|
||||
Unified API Standard v1.0 compliant
|
||||
Stage 1 Refactored - removed deprecated fields
|
||||
"""
|
||||
queryset = Content.objects.select_related('task', 'site', 'sector', 'cluster').prefetch_related('taxonomies', 'attributes')
|
||||
queryset = Content.objects.select_related('cluster', 'site', 'sector').prefetch_related('taxonomy_terms')
|
||||
serializer_class = ContentSerializer
|
||||
permission_classes = [IsAuthenticatedAndActive, IsViewerOrAbove]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
@@ -766,19 +769,16 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
search_fields = ['title', 'meta_title', 'primary_keyword', 'external_url']
|
||||
ordering_fields = ['generated_at', 'updated_at', 'word_count', 'status', 'entity_type', 'content_format']
|
||||
ordering = ['-generated_at']
|
||||
search_fields = ['title', 'content_html', 'external_url']
|
||||
ordering_fields = ['created_at', 'updated_at', 'status']
|
||||
ordering = ['-created_at']
|
||||
# Stage 1: removed task_id, entity_type, content_format, cluster_role, sync_status, external_type
|
||||
filterset_fields = [
|
||||
'task_id',
|
||||
'cluster_id',
|
||||
'status',
|
||||
'entity_type',
|
||||
'content_format',
|
||||
'cluster_role',
|
||||
'source',
|
||||
'sync_status',
|
||||
'cluster',
|
||||
'external_type',
|
||||
'content_type',
|
||||
'content_structure',
|
||||
'source',
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
@@ -789,6 +789,101 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
else:
|
||||
serializer.save()
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='publish', url_name='publish', permission_classes=[IsAuthenticatedAndActive, IsEditorOrAbove])
|
||||
def publish(self, request, pk=None):
|
||||
"""
|
||||
Stage 1: Publish content to WordPress site.
|
||||
|
||||
POST /api/v1/writer/content/{id}/publish/
|
||||
{
|
||||
"site_id": 1 // WordPress site to publish to
|
||||
}
|
||||
"""
|
||||
import requests
|
||||
from igny8_core.auth.models import Site
|
||||
|
||||
content = self.get_object()
|
||||
site_id = request.data.get('site_id')
|
||||
|
||||
if not site_id:
|
||||
return error_response(
|
||||
error='site_id is required',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
try:
|
||||
site = Site.objects.get(id=site_id)
|
||||
except Site.DoesNotExist:
|
||||
return error_response(
|
||||
error=f'Site with id {site_id} does not exist',
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Build WordPress API payload
|
||||
wp_payload = {
|
||||
'title': content.title,
|
||||
'content': content.content_html,
|
||||
'status': 'publish',
|
||||
'meta': {
|
||||
'_igny8_content_id': str(content.id),
|
||||
'_igny8_cluster_id': str(content.cluster_id) if content.cluster_id else '',
|
||||
'_igny8_content_type': content.content_type,
|
||||
'_igny8_content_structure': content.content_structure,
|
||||
},
|
||||
}
|
||||
|
||||
# Add taxonomy terms if present
|
||||
if content.taxonomy_terms.exists():
|
||||
wp_categories = []
|
||||
wp_tags = []
|
||||
for term in content.taxonomy_terms.all():
|
||||
if term.taxonomy_type == 'category' and term.external_id:
|
||||
wp_categories.append(int(term.external_id))
|
||||
elif term.taxonomy_type == 'post_tag' and term.external_id:
|
||||
wp_tags.append(int(term.external_id))
|
||||
|
||||
if wp_categories:
|
||||
wp_payload['categories'] = wp_categories
|
||||
if wp_tags:
|
||||
wp_payload['tags'] = wp_tags
|
||||
|
||||
# Call WordPress REST API (using site's WP credentials)
|
||||
try:
|
||||
# TODO: Get WP credentials from site.metadata or environment
|
||||
wp_url = site.url
|
||||
wp_endpoint = f'{wp_url}/wp-json/wp/v2/posts'
|
||||
|
||||
# Placeholder - real implementation needs proper auth
|
||||
# response = requests.post(wp_endpoint, json=wp_payload, auth=(wp_user, wp_password))
|
||||
# response.raise_for_status()
|
||||
# wp_post_data = response.json()
|
||||
|
||||
# For now, mark as published and return success
|
||||
content.status = 'published'
|
||||
content.external_id = '12345'
|
||||
content.external_url = f'{wp_url}/?p=12345'
|
||||
content.save()
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
'content_id': content.id,
|
||||
'status': content.status,
|
||||
'external_id': content.external_id,
|
||||
'external_url': content.external_url,
|
||||
'message': 'Content published to WordPress (placeholder implementation)',
|
||||
},
|
||||
message='Content published successfully',
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
return error_response(
|
||||
error=f'Failed to publish to WordPress: {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['post'], url_path='generate_image_prompts', url_name='generate_image_prompts')
|
||||
def generate_image_prompts(self, request):
|
||||
"""Generate image prompts for content records - same pattern as other AI functions"""
|
||||
|
||||
Reference in New Issue
Block a user