330 lines
11 KiB
Python
330 lines
11 KiB
Python
from rest_framework import serializers
|
|
from django.db import models
|
|
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
|
|
from igny8_core.business.content.models import ContentTaxonomy
|
|
|
|
|
|
class TasksSerializer(serializers.ModelSerializer):
|
|
"""Serializer for Tasks model - Stage 1 refactored"""
|
|
cluster_name = serializers.SerializerMethodField()
|
|
sector_name = serializers.SerializerMethodField()
|
|
site_id = serializers.IntegerField(write_only=True, required=False)
|
|
sector_id = serializers.IntegerField(write_only=True, required=False)
|
|
|
|
class Meta:
|
|
model = Tasks
|
|
fields = [
|
|
'id',
|
|
'title',
|
|
'description',
|
|
'cluster_id',
|
|
'cluster_name',
|
|
'content_type',
|
|
'content_structure',
|
|
'taxonomy_term_id',
|
|
'word_count',
|
|
'status',
|
|
'sector_name',
|
|
'site_id',
|
|
'sector_id',
|
|
'account_id',
|
|
'created_at',
|
|
'updated_at',
|
|
]
|
|
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id']
|
|
|
|
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"""
|
|
if obj.cluster_id:
|
|
try:
|
|
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"""
|
|
if obj.sector_id:
|
|
try:
|
|
from igny8_core.auth.models import Sector
|
|
sector = Sector.objects.get(id=obj.sector_id)
|
|
return sector.name
|
|
except Sector.DoesNotExist:
|
|
return None
|
|
return None
|
|
|
|
|
|
class ImagesSerializer(serializers.ModelSerializer):
|
|
"""Serializer for Images model"""
|
|
task_title = serializers.SerializerMethodField()
|
|
content_title = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Images
|
|
fields = [
|
|
'id',
|
|
'task_id',
|
|
'task_title',
|
|
'content_id',
|
|
'content_title',
|
|
'image_type',
|
|
'image_url',
|
|
'image_path',
|
|
'prompt',
|
|
'status',
|
|
'position',
|
|
'created_at',
|
|
'updated_at',
|
|
'account_id',
|
|
]
|
|
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id']
|
|
|
|
def get_task_title(self, obj):
|
|
"""Get task title"""
|
|
if obj.task_id:
|
|
try:
|
|
task = Tasks.objects.get(id=obj.task_id)
|
|
return task.title
|
|
except Tasks.DoesNotExist:
|
|
return None
|
|
return None
|
|
|
|
def get_content_title(self, obj):
|
|
"""Get content title"""
|
|
if obj.content:
|
|
return obj.content.title or obj.content.meta_title
|
|
return None
|
|
|
|
|
|
class ContentImageSerializer(serializers.ModelSerializer):
|
|
"""Serializer for individual image in grouped content images"""
|
|
image_url = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Images
|
|
fields = [
|
|
'id',
|
|
'image_type',
|
|
'image_url',
|
|
'image_path',
|
|
'prompt',
|
|
'status',
|
|
'position',
|
|
'created_at',
|
|
'updated_at',
|
|
]
|
|
|
|
def get_image_url(self, obj):
|
|
"""
|
|
Return original image_url from database (Runware/OpenAI URL).
|
|
No transformation - returns the exact value stored in image_url field.
|
|
"""
|
|
return obj.image_url
|
|
|
|
|
|
class ContentImagesGroupSerializer(serializers.Serializer):
|
|
"""Serializer for grouped content images - one row per content"""
|
|
content_id = serializers.IntegerField()
|
|
content_title = serializers.CharField()
|
|
featured_image = ContentImageSerializer(allow_null=True)
|
|
in_article_images = ContentImageSerializer(many=True)
|
|
overall_status = serializers.CharField() # 'pending', 'partial', 'complete', 'failed'
|
|
|
|
|
|
class ContentSerializer(serializers.ModelSerializer):
|
|
"""Serializer for Content model - Stage 1 refactored"""
|
|
cluster_name = serializers.SerializerMethodField()
|
|
sector_name = serializers.SerializerMethodField()
|
|
taxonomy_terms_data = serializers.SerializerMethodField()
|
|
tags = serializers.SerializerMethodField()
|
|
categories = serializers.SerializerMethodField()
|
|
has_image_prompts = serializers.SerializerMethodField()
|
|
image_status = serializers.SerializerMethodField()
|
|
has_generated_images = 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',
|
|
'title',
|
|
'content_html',
|
|
'meta_title',
|
|
'meta_description',
|
|
'primary_keyword',
|
|
'secondary_keywords',
|
|
'cluster_id',
|
|
'cluster_name',
|
|
'content_type',
|
|
'content_structure',
|
|
'taxonomy_terms_data',
|
|
'tags',
|
|
'categories',
|
|
'external_id',
|
|
'external_url',
|
|
'source',
|
|
'status',
|
|
'word_count',
|
|
'sector_name',
|
|
'site_id',
|
|
'sector_id',
|
|
'account_id',
|
|
'has_image_prompts',
|
|
'image_status',
|
|
'has_generated_images',
|
|
'created_at',
|
|
'updated_at',
|
|
]
|
|
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id']
|
|
|
|
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_cluster_name(self, obj):
|
|
"""Get cluster name"""
|
|
if obj.cluster_id:
|
|
try:
|
|
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"""
|
|
if obj.sector_id:
|
|
try:
|
|
from igny8_core.auth.models import Sector
|
|
sector = Sector.objects.get(id=obj.sector_id)
|
|
return sector.name
|
|
except Sector.DoesNotExist:
|
|
return None
|
|
return None
|
|
|
|
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()
|
|
]
|
|
|
|
def get_tags(self, obj):
|
|
"""Get only tags (taxonomy_type='tag')"""
|
|
return [
|
|
term.name
|
|
for term in obj.taxonomy_terms.filter(taxonomy_type='tag')
|
|
]
|
|
|
|
def get_categories(self, obj):
|
|
"""Get only categories (taxonomy_type='category')"""
|
|
return [
|
|
term.name
|
|
for term in obj.taxonomy_terms.filter(taxonomy_type='category')
|
|
]
|
|
|
|
def get_has_image_prompts(self, obj):
|
|
"""Check if content has any image prompts (images with prompts)"""
|
|
return obj.images.filter(prompt__isnull=False).exclude(prompt='').exists()
|
|
|
|
def get_image_status(self, obj):
|
|
"""Get image generation status: 'generated', 'pending', 'failed', or None"""
|
|
images = obj.images.all()
|
|
if not images.exists():
|
|
return None
|
|
|
|
# Check statuses
|
|
has_failed = images.filter(status='failed').exists()
|
|
has_generated = images.filter(status='generated').exists()
|
|
has_pending = images.filter(status='pending').exists()
|
|
|
|
# Priority: failed > pending > generated
|
|
if has_failed:
|
|
return 'failed'
|
|
elif has_pending:
|
|
return 'pending'
|
|
elif has_generated:
|
|
return 'generated'
|
|
|
|
return None
|
|
|
|
def get_has_generated_images(self, obj):
|
|
"""Check if content has any successfully generated images"""
|
|
return obj.images.filter(status='generated', image_url__isnull=False).exclude(image_url='').exists()
|
|
|
|
|
|
class ContentTaxonomySerializer(serializers.ModelSerializer):
|
|
"""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
|
|
fields = [
|
|
'id',
|
|
'name',
|
|
'slug',
|
|
'taxonomy_type',
|
|
'external_id',
|
|
'external_taxonomy',
|
|
'content_count',
|
|
'site_id',
|
|
'sector_id',
|
|
'account_id',
|
|
'created_at',
|
|
'updated_at',
|
|
]
|
|
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id']
|
|
|
|
def get_content_count(self, obj):
|
|
"""Get count of content using this taxonomy"""
|
|
return obj.contents.count()
|
|
|
|
|
|
# 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
|