from rest_framework import viewsets, status from rest_framework.decorators import action, api_view, permission_classes from rest_framework.response import Response from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.parsers import MultiPartParser, FormParser, JSONParser from rest_framework.authtoken.models import Token from django.shortcuts import get_object_or_404 from django.db.models import Q from django.utils import timezone from django.utils.dateparse import parse_datetime from .models import ContactUs, Composition, Campaign, CompositionImage from .serializers import ( ContactUsSerializer, CompositionSerializer, CampaignSerializer, CompositionImageSerializer, AdminLoginSerializer, ) @api_view(['POST']) @permission_classes([AllowAny]) def admin_login(request): """ Admin login with username and password. Returns an auth token to use as: Authorization: Token """ serializer = AdminLoginSerializer(data=request.data) serializer.is_valid(raise_exception=True) user = serializer.validated_data['user'] token, _ = Token.objects.get_or_create(user=user) return Response({ 'token': token.key, 'user': { 'id': user.id, 'username': user.username, 'is_staff': user.is_staff, 'is_superuser': user.is_superuser, }, }) @api_view(['POST']) @permission_classes([IsAuthenticated]) def admin_logout(request): """Delete the current admin auth token (logout).""" Token.objects.filter(user=request.user).delete() return Response({'detail': 'Logged out successfully.'}) @api_view(['GET']) @permission_classes([IsAuthenticated]) def admin_me(request): """Return the currently authenticated admin user.""" user = request.user return Response({ 'id': user.id, 'username': user.username, 'is_staff': user.is_staff, 'is_superuser': user.is_superuser, 'is_active': user.is_active, }) class ContactUsViewSet(viewsets.ModelViewSet): """ ViewSet for ContactUs model. Provides CRUD operations for contact form submissions. """ queryset = ContactUs.objects.all() serializer_class = ContactUsSerializer permission_classes = [AllowAny] # Allow anyone to submit contact forms def get_permissions(self): """ Override to allow GET (list, retrieve) and POST (create) for anyone. """ if self.action in ['list', 'retrieve', 'create', 'by_category']: return [AllowAny()] return [IsAuthenticated()] @action(detail=False, methods=['get']) def by_category(self, request): """Get contacts filtered by category.""" category = request.query_params.get('category', None) if category: contacts = self.queryset.filter(category=category) serializer = self.get_serializer(contacts, many=True) return Response(serializer.data) return Response( {'error': 'Category parameter is required.'}, status=status.HTTP_400_BAD_REQUEST ) class CompositionViewSet(viewsets.ModelViewSet): """ ViewSet for Composition model. Supports uploading one or more images at create/update time via the multipart ``uploaded_images`` field, with an optional ``main_image_index`` to flag the main image. Extra actions allow adding images, choosing the main image, and deleting an image after creation. """ queryset = Composition.objects.prefetch_related('images').all() serializer_class = CompositionSerializer permission_classes = [AllowAny] # Public read access parser_classes = [MultiPartParser, FormParser, JSONParser] def get_permissions(self): """ Override to allow GET (list, retrieve) and POST (create) for anyone. """ if self.action in ['list', 'retrieve', 'create', 'by_created_at']: return [AllowAny()] return [IsAuthenticated()] def get_serializer_context(self): """Add request to serializer context for image URL generation.""" context = super().get_serializer_context() context['request'] = self.request return context @action(detail=False, methods=['get'], url_path='by-created-at') def by_created_at(self, request): """Get compositions filtered by created_at date range.""" from_param = request.query_params.get('from') to_param = request.query_params.get('to') if not from_param and not to_param: return Response( {'error': 'At least one of "from" or "to" query parameters is required.'}, status=status.HTTP_400_BAD_REQUEST, ) compositions = self.queryset if from_param: from_dt = parse_datetime(from_param) if from_dt is None: return Response( {'error': 'Invalid "from" datetime. Use ISO 8601 format.'}, status=status.HTTP_400_BAD_REQUEST, ) if timezone.is_naive(from_dt): from_dt = timezone.make_aware(from_dt) compositions = compositions.filter(created_at__gte=from_dt) if to_param: to_dt = parse_datetime(to_param) if to_dt is None: return Response( {'error': 'Invalid "to" datetime. Use ISO 8601 format.'}, status=status.HTTP_400_BAD_REQUEST, ) if timezone.is_naive(to_dt): to_dt = timezone.make_aware(to_dt) compositions = compositions.filter(created_at__lte=to_dt) serializer = self.get_serializer(compositions, many=True) return Response(serializer.data) def _composition_response(self, composition): """Return a fresh composition payload with up-to-date images.""" composition = Composition.objects.prefetch_related('images').get( pk=composition.pk ) return self.get_serializer(composition).data @action(detail=True, methods=['post'], url_path='add-images') def add_images(self, request, pk=None): """Add one or more images to an existing composition.""" composition = self.get_object() serializer = self.get_serializer( composition, data=request.data, partial=True ) serializer.is_valid(raise_exception=True) composition = serializer.save() return Response(self._composition_response(composition)) @action(detail=True, methods=['post'], url_path='set-main-image') def set_main_image(self, request, pk=None): """Flag one of the composition's images as the main image.""" composition = self.get_object() image_id = request.data.get('image_id') if image_id is None: return Response( {'error': 'image_id is required.'}, status=status.HTTP_400_BAD_REQUEST ) image = get_object_or_404( CompositionImage, pk=image_id, composition=composition ) image.is_main = True image.save() # model.save() unsets is_main on the other images return Response(self._composition_response(composition)) @action( detail=True, methods=['delete'], url_path='images/(?P[^/.]+)' ) def delete_image(self, request, pk=None, image_id=None): """Delete a single image from the composition.""" composition = self.get_object() image = get_object_or_404( CompositionImage, pk=image_id, composition=composition ) image.delete() return Response(status=status.HTTP_204_NO_CONTENT) class CampaignViewSet(viewsets.ModelViewSet): """ ViewSet for Campaign model. Provides CRUD operations for campaigns. """ queryset = Campaign.objects.all() serializer_class = CampaignSerializer permission_classes = [AllowAny] # Public read access def get_permissions(self): """ Override to allow GET (list, retrieve) and POST (create) for anyone. """ if self.action in ['list', 'retrieve', 'create', 'active', 'upcoming', 'ended']: return [AllowAny()] return [IsAuthenticated()] def get_serializer_context(self): """Add request to serializer context for image URL generation.""" context = super().get_serializer_context() context['request'] = self.request return context @action(detail=False, methods=['get']) def active(self, request): """Get all currently active campaigns.""" now = timezone.now() active_campaigns = self.queryset.filter( start_time__lte=now, end_time__gte=now ) serializer = self.get_serializer(active_campaigns, many=True) return Response(serializer.data) @action(detail=False, methods=['get']) def upcoming(self, request): """Get all upcoming campaigns.""" now = timezone.now() upcoming_campaigns = self.queryset.filter(start_time__gt=now) serializer = self.get_serializer(upcoming_campaigns, many=True) return Response(serializer.data) @action(detail=False, methods=['get']) def ended(self, request): """Get all ended campaigns.""" now = timezone.now() ended_campaigns = self.queryset.filter(end_time__lt=now) serializer = self.get_serializer(ended_campaigns, many=True) return Response(serializer.data)