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 # 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 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', 'primary_focus_keywords', '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