- Updated AutomationService to include estimated_word_count. - Increased stage_1_batch_size from 20 to 50 in AutomationViewSet. - Changed Keywords model to replace 'intent' property with 'country'. - Adjusted ClusteringService to allow a maximum of 50 keywords for clustering. - Modified admin and management commands to remove 'intent' and use 'country' instead. - Updated serializers to reflect the change from 'intent' to 'country'. - Adjusted views and filters to use 'country' instead of 'intent'. - Updated frontend forms, filters, and pages to replace 'intent' with 'country'. - Added migration to remove 'intent' field and add 'country' field to SeedKeyword model.
303 lines
12 KiB
Python
303 lines
12 KiB
Python
from rest_framework import serializers
|
|
from django.conf import settings
|
|
|
|
from .models import Keywords, Clusters, ContentIdeas
|
|
from igny8_core.auth.models import SeedKeyword
|
|
from igny8_core.auth.serializers import SeedKeywordSerializer
|
|
# Removed: from igny8_core.business.site_building.models import SiteBlueprintTaxonomy
|
|
|
|
|
|
class KeywordSerializer(serializers.ModelSerializer):
|
|
"""Serializer for Keywords model with SeedKeyword relationship"""
|
|
# Read-only properties from seed_keyword
|
|
keyword = serializers.CharField(read_only=True) # From seed_keyword.keyword
|
|
volume = serializers.IntegerField(read_only=True) # From seed_keyword.volume or volume_override
|
|
difficulty = serializers.IntegerField(read_only=True) # From seed_keyword.difficulty or difficulty_override
|
|
country = serializers.CharField(read_only=True) # From seed_keyword.country
|
|
|
|
# SeedKeyword relationship
|
|
# Optional for create - can either provide seed_keyword_id OR custom keyword fields
|
|
seed_keyword_id = serializers.IntegerField(write_only=True, required=False)
|
|
seed_keyword = SeedKeywordSerializer(read_only=True)
|
|
|
|
# Custom keyword fields (write-only, for creating new seed keywords on-the-fly)
|
|
custom_keyword = serializers.CharField(write_only=True, required=False, allow_blank=False)
|
|
custom_volume = serializers.IntegerField(write_only=True, required=False, allow_null=True)
|
|
custom_difficulty = serializers.IntegerField(write_only=True, required=False, allow_null=True)
|
|
custom_country = serializers.ChoiceField(
|
|
write_only=True,
|
|
required=False,
|
|
choices=['US', 'CA', 'GB', 'AE', 'AU', 'IN', 'PK'],
|
|
default='US'
|
|
)
|
|
|
|
# Overrides
|
|
volume_override = serializers.IntegerField(required=False, allow_null=True)
|
|
difficulty_override = serializers.IntegerField(required=False, allow_null=True)
|
|
|
|
# Related fields
|
|
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 = Keywords
|
|
fields = [
|
|
'id',
|
|
'seed_keyword_id',
|
|
'seed_keyword',
|
|
'keyword',
|
|
'volume',
|
|
'difficulty',
|
|
'country',
|
|
'custom_keyword', # Write-only field for creating custom keywords
|
|
'custom_volume', # Write-only
|
|
'custom_difficulty', # Write-only
|
|
'custom_country', # Write-only
|
|
'volume_override',
|
|
'difficulty_override',
|
|
'cluster_id',
|
|
'cluster_name',
|
|
'sector_name',
|
|
'status',
|
|
'created_at',
|
|
'updated_at',
|
|
'site_id',
|
|
'sector_id',
|
|
'account_id',
|
|
]
|
|
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id', 'keyword', 'volume', 'difficulty', 'country']
|
|
|
|
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['attribute_values'] = serializers.JSONField(read_only=True)
|
|
|
|
def validate(self, attrs):
|
|
"""Validate that either seed_keyword_id OR custom keyword fields are provided"""
|
|
# For create operations, need either seed_keyword_id OR custom keyword
|
|
if self.instance is None:
|
|
has_seed_keyword = 'seed_keyword_id' in attrs
|
|
has_custom_keyword = 'custom_keyword' in attrs
|
|
|
|
if not has_seed_keyword and not has_custom_keyword:
|
|
raise serializers.ValidationError({
|
|
'keyword': 'Either provide seed_keyword_id to link an existing keyword, or provide custom_keyword to create a new one.'
|
|
})
|
|
|
|
if has_custom_keyword:
|
|
# Validate custom keyword fields
|
|
if not attrs.get('custom_keyword', '').strip():
|
|
raise serializers.ValidationError({'custom_keyword': 'Keyword text cannot be empty.'})
|
|
|
|
if attrs.get('custom_volume') is None:
|
|
raise serializers.ValidationError({'custom_volume': 'Volume is required when creating a custom keyword.'})
|
|
|
|
if attrs.get('custom_difficulty') is None:
|
|
raise serializers.ValidationError({'custom_difficulty': 'Difficulty is required when creating a custom keyword.'})
|
|
|
|
return attrs
|
|
|
|
def create(self, validated_data):
|
|
"""Create Keywords instance with seed_keyword (existing or newly created)"""
|
|
# Extract custom keyword fields
|
|
custom_keyword = validated_data.pop('custom_keyword', None)
|
|
custom_volume = validated_data.pop('custom_volume', None)
|
|
custom_difficulty = validated_data.pop('custom_difficulty', None)
|
|
custom_country = validated_data.pop('custom_country', None) or 'US'
|
|
|
|
# Get site and sector - they're passed as objects via save() in the view
|
|
site = validated_data.get('site')
|
|
sector = validated_data.get('sector')
|
|
|
|
if not site or not sector:
|
|
raise serializers.ValidationError('Site and sector are required.')
|
|
|
|
# Determine which seed_keyword to use
|
|
if custom_keyword:
|
|
# Create or get SeedKeyword for this custom keyword
|
|
if not site.industry:
|
|
raise serializers.ValidationError({'site': 'Site must have an industry assigned.'})
|
|
|
|
if not sector.industry_sector:
|
|
raise serializers.ValidationError({'sector': 'Sector must have an industry_sector assigned.'})
|
|
|
|
# Get or create the SeedKeyword
|
|
seed_keyword, created = SeedKeyword.objects.get_or_create(
|
|
keyword=custom_keyword.strip().lower(),
|
|
industry=site.industry,
|
|
sector=sector.industry_sector,
|
|
defaults={
|
|
'volume': custom_volume or 0,
|
|
'difficulty': custom_difficulty or 0,
|
|
'country': custom_country or 'US',
|
|
'is_active': True,
|
|
}
|
|
)
|
|
|
|
# If it existed, optionally update values (or keep existing ones)
|
|
# For now, we'll keep existing values if the seed keyword already exists
|
|
|
|
validated_data['seed_keyword'] = seed_keyword
|
|
else:
|
|
# Use provided seed_keyword_id
|
|
seed_keyword_id = validated_data.pop('seed_keyword_id', None)
|
|
if not seed_keyword_id:
|
|
raise serializers.ValidationError({'seed_keyword_id': 'This field is required when not providing a custom keyword.'})
|
|
|
|
try:
|
|
seed_keyword = SeedKeyword.objects.get(id=seed_keyword_id)
|
|
except SeedKeyword.DoesNotExist:
|
|
raise serializers.ValidationError({'seed_keyword_id': f'SeedKeyword with id {seed_keyword_id} does not exist'})
|
|
|
|
validated_data['seed_keyword'] = seed_keyword
|
|
|
|
return super().create(validated_data)
|
|
|
|
def update(self, instance, validated_data):
|
|
"""Update Keywords instance - only cluster_id and status can be updated"""
|
|
# Remove custom keyword fields if present (they shouldn't be in update)
|
|
validated_data.pop('custom_keyword', None)
|
|
validated_data.pop('custom_volume', None)
|
|
validated_data.pop('custom_difficulty', None)
|
|
validated_data.pop('custom_country', None)
|
|
|
|
# seed_keyword_id is optional for updates - only update if provided
|
|
if 'seed_keyword_id' in validated_data:
|
|
seed_keyword_id = validated_data.pop('seed_keyword_id')
|
|
try:
|
|
seed_keyword = SeedKeyword.objects.get(id=seed_keyword_id)
|
|
validated_data['seed_keyword'] = seed_keyword
|
|
except SeedKeyword.DoesNotExist:
|
|
raise serializers.ValidationError({'seed_keyword_id': f'SeedKeyword with id {seed_keyword_id} does not exist'})
|
|
|
|
return super().update(instance, validated_data)
|
|
|
|
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 ClusterSerializer(serializers.ModelSerializer):
|
|
"""Serializer for Clusters model - pure topic clusters"""
|
|
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 = Clusters
|
|
fields = [
|
|
'id',
|
|
'name',
|
|
'description',
|
|
'keywords_count',
|
|
'volume',
|
|
'mapped_pages',
|
|
'status',
|
|
'sector_name',
|
|
'created_at',
|
|
'updated_at',
|
|
'site_id',
|
|
'sector_id',
|
|
'account_id',
|
|
]
|
|
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id', 'keywords_count', 'volume', 'mapped_pages']
|
|
|
|
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
|
|
|
|
def validate_name(self, value):
|
|
"""Ensure cluster name is unique within account"""
|
|
# Uniqueness is handled at model level, but we can add additional validation here
|
|
return value
|
|
|
|
|
|
class ContentIdeasSerializer(serializers.ModelSerializer):
|
|
"""Serializer for ContentIdeas model"""
|
|
keyword_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 = ContentIdeas
|
|
fields = [
|
|
'id',
|
|
'idea_title',
|
|
'description',
|
|
'content_type',
|
|
'content_structure',
|
|
'target_keywords',
|
|
'keyword_cluster_id',
|
|
'keyword_cluster_name',
|
|
'sector_name',
|
|
'status',
|
|
'estimated_word_count',
|
|
'created_at',
|
|
'updated_at',
|
|
'site_id',
|
|
'sector_id',
|
|
'account_id',
|
|
]
|
|
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id']
|
|
|
|
def get_keyword_cluster_name(self, obj):
|
|
"""Get cluster name from Clusters model"""
|
|
if obj.keyword_cluster_id:
|
|
try:
|
|
cluster = Clusters.objects.get(id=obj.keyword_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
|
|
|
|
def get_taxonomy_name(self, obj):
|
|
"""Legacy: SiteBlueprintTaxonomy removed - taxonomy now in ContentTaxonomy"""
|
|
if obj.taxonomy_id:
|
|
try:
|
|
from igny8_core.business.content.models import ContentTaxonomy
|
|
taxonomy = ContentTaxonomy.objects.get(id=obj.taxonomy_id)
|
|
return taxonomy.name
|
|
except ContentTaxonomy.DoesNotExist:
|
|
return None
|
|
return None
|
|
|