from rest_framework import serializers from .models import ContactUs, Composition, Campaign, CompositionImage class ContactUsSerializer(serializers.ModelSerializer): """ Serializer for ContactUs model. """ class Meta: model = ContactUs fields = ['id', 'name', 'email_or_phone', 'description', 'category', 'created_at', 'updated_at'] read_only_fields = ['id', 'created_at', 'updated_at'] def validate_email_or_phone(self, value): """Validate that the field contains either email or phone.""" if not value or len(value.strip()) == 0: raise serializers.ValidationError("Email or phone number is required.") return value.strip() def validate_name(self, value): """Validate name field.""" if not value or len(value.strip()) == 0: raise serializers.ValidationError("Name is required.") return value.strip() def validate_description(self, value): """Validate description field.""" if not value or len(value.strip()) == 0: raise serializers.ValidationError("Description is required.") return value.strip() # Accepted image content types and max upload size (5 MB). ALLOWED_IMAGE_CONTENT_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'] MAX_IMAGE_SIZE = 5 * 1024 * 1024 def validate_uploaded_image(image): """Reject anything that is not a real, reasonably sized image file.""" content_type = getattr(image, 'content_type', None) if content_type and content_type not in ALLOWED_IMAGE_CONTENT_TYPES: raise serializers.ValidationError( f"Unsupported image type '{content_type}'. " f"Allowed: {', '.join(ALLOWED_IMAGE_CONTENT_TYPES)}." ) if image.size > MAX_IMAGE_SIZE: raise serializers.ValidationError( f"Image too large ({image.size} bytes). Max allowed is {MAX_IMAGE_SIZE} bytes." ) return image class CompositionImageSerializer(serializers.ModelSerializer): """ Serializer for a single image belonging to a Composition. """ image_url = serializers.SerializerMethodField() class Meta: model = CompositionImage fields = ['id', 'image', 'image_url', 'is_main', 'created_at'] read_only_fields = ['id', 'image_url', 'created_at'] def get_image_url(self, obj): """Get the full URL of the image.""" if obj.image: request = self.context.get('request') if request: return request.build_absolute_uri(obj.image.url) return obj.image.url return None class CompositionSerializer(serializers.ModelSerializer): """ Serializer for Composition model. Read: returns the nested ``images`` list plus ``main_image``. Write: accepts ``uploaded_images`` (one or more files) and an optional ``main_image_index`` pointing at which uploaded file is the main image. """ # Legacy single image URL (kept for backward compatibility). image_url = serializers.SerializerMethodField() images = CompositionImageSerializer(many=True, read_only=True) main_image = serializers.SerializerMethodField() # Write-only upload fields. uploaded_images = serializers.ListField( child=serializers.ImageField(validators=[validate_uploaded_image]), write_only=True, required=False, ) main_image_index = serializers.IntegerField( write_only=True, required=False, min_value=0, help_text='0-based index into uploaded_images marking the main image.' ) class Meta: model = Composition fields = ['id', 'name', 'description', 'image', 'image_url', 'images', 'main_image', 'uploaded_images', 'main_image_index', 'created_at', 'updated_at'] read_only_fields = ['id', 'created_at', 'updated_at', 'image_url', 'images', 'main_image'] def get_image_url(self, obj): """Get the full URL of the legacy image field.""" if obj.image: request = self.context.get('request') if request: return request.build_absolute_uri(obj.image.url) return obj.image.url return None def get_main_image(self, obj): """Return the serialized main image, if any.""" main = obj.main_image if main: return CompositionImageSerializer(main, context=self.context).data return None def validate_name(self, value): """Validate name field.""" if not value or len(value.strip()) == 0: raise serializers.ValidationError("Name is required.") return value.strip() def validate_description(self, value): """Validate description field.""" if not value or len(value.strip()) == 0: raise serializers.ValidationError("Description is required.") return value.strip() def to_internal_value(self, data): """ MultiPartParser stores repeated file fields as a QueryDict where only the last value is returned by .get(). Pull the full list explicitly. """ if hasattr(data, 'getlist'): uploaded = data.getlist('uploaded_images') if uploaded: mutable = data.copy() mutable.setlist('uploaded_images', uploaded) data = mutable return super().to_internal_value(data) def validate(self, data): """Ensure main_image_index points at an actually uploaded image.""" uploaded = data.get('uploaded_images') index = data.get('main_image_index') if index is not None: if not uploaded: raise serializers.ValidationError({ 'main_image_index': 'Cannot set a main image without uploaded_images.' }) if index >= len(uploaded): raise serializers.ValidationError({ 'main_image_index': f'Index out of range (got {index}, ' f'{len(uploaded)} image(s) uploaded).' }) return data def _save_images(self, composition, uploaded_images, main_index): """Persist uploaded images, flagging the chosen one as main.""" # If the composition has no main image yet, default the first upload to main. has_main = composition.images.filter(is_main=True).exists() for i, image_file in enumerate(uploaded_images): is_main = (i == main_index) if main_index is not None else ( i == 0 and not has_main ) CompositionImage.objects.create( composition=composition, image=image_file, is_main=is_main, ) def create(self, validated_data): uploaded_images = validated_data.pop('uploaded_images', []) main_index = validated_data.pop('main_image_index', None) composition = super().create(validated_data) if uploaded_images: self._save_images(composition, uploaded_images, main_index) return composition def update(self, instance, validated_data): uploaded_images = validated_data.pop('uploaded_images', []) main_index = validated_data.pop('main_image_index', None) composition = super().update(instance, validated_data) if uploaded_images: self._save_images(composition, uploaded_images, main_index) return composition class CampaignSerializer(serializers.ModelSerializer): """ Serializer for Campaign model. """ image_url = serializers.SerializerMethodField() is_active = serializers.SerializerMethodField() is_upcoming = serializers.SerializerMethodField() is_ended = serializers.SerializerMethodField() class Meta: model = Campaign fields = ['id', 'name', 'description', 'start_time', 'end_time', 'image', 'image_url', 'is_active', 'is_upcoming', 'is_ended', 'created_at', 'updated_at'] read_only_fields = ['id', 'created_at', 'updated_at', 'image_url', 'is_active', 'is_upcoming', 'is_ended'] def get_image_url(self, obj): """Get the full URL of the image.""" if obj.image: request = self.context.get('request') if request: return request.build_absolute_uri(obj.image.url) return obj.image.url return None def get_is_active(self, obj): """Get campaign active status.""" return obj.is_active() def get_is_upcoming(self, obj): """Get campaign upcoming status.""" return obj.is_upcoming() def get_is_ended(self, obj): """Get campaign ended status.""" return obj.is_ended() def validate(self, data): """Validate that end_time is after start_time.""" if 'start_time' in data and 'end_time' in data: if data['end_time'] <= data['start_time']: raise serializers.ValidationError({ 'end_time': 'End time must be after start time.' }) return data def validate_name(self, value): """Validate name field.""" if not value or len(value.strip()) == 0: raise serializers.ValidationError("Name is required.") return value.strip() def validate_description(self, value): """Validate description field.""" if not value or len(value.strip()) == 0: raise serializers.ValidationError("Description is required.") return value.strip()