from django.core.mail import send_mail from rest_framework import viewsets, permissions, status from rest_framework.response import Response from rest_framework.decorators import action from django.db.models import Avg from django_filters.rest_framework import DjangoFilterBackend from .models import User, Flavor, Category, Rating, Reply from .serializers import UserSerializer, FlavorSerializer, CategorySerializer, RatingSerializer, ReplySerializer class FlavorViewSet(viewsets.ModelViewSet): queryset = Flavor.objects.annotate(average_rating=Avg('ratings__score')).order_by('-average_rating') serializer_class = FlavorSerializer filter_backends = [DjangoFilterBackend] filterset_fields = ['category', 'category__slug'] permission_classes = [permissions.IsAuthenticatedOrReadOnly] def get_queryset(self): return Flavor.objects.select_related('category').annotate(average_rating=Avg('ratings__score')).order_by('-average_rating') class CategoryViewSet(viewsets.ReadOnlyModelViewSet): queryset = Category.objects.all() serializer_class = CategorySerializer class RatingViewSet(viewsets.ModelViewSet): queryset = Rating.objects.all() serializer_class = RatingSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] def perform_create(self, serializer): # Ensure user can only have one rating per flavor flavor = serializer.validated_data['flavor'] if Rating.objects.filter(user=self.request.user, flavor=flavor).exists(): raise serializers.ValidationError('You have already rated this flavor.') serializer.save(user=self.request.user) def get_queryset(self): if self.action in ['list', 'retrieve']: return Rating.objects.select_related('user', 'flavor').prefetch_related('replies', 'replies__user') # Allow users to see only their own ratings when editing/deleting? # No, for detail views they need to find it. # The permission class IsOwnerOrReadOnly (custom) would be better, # but standard ViewSet logic handles "get_object" which we can protect. return Rating.objects.select_related('user', 'flavor').prefetch_related('replies', 'replies__user') def check_object_permissions(self, request, obj): super().check_object_permissions(request, obj) if request.method in ['PUT', 'PATCH', 'DELETE'] and obj.user != request.user: self.permission_denied(request, message='You cannot edit/delete this rating.') @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) def reply(self, request, pk=None): rating = self.get_object() text = request.data.get('text') if not text: return Response({'error': 'Text is required'}, status=status.HTTP_400_BAD_REQUEST) reply = Reply.objects.create(user=request.user, rating=rating, text=text) return Response(ReplySerializer(reply).data, status=status.HTTP_201_CREATED) class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = [permissions.IsAuthenticated] def get_permissions(self): if self.action in ['signup', 'verify_signup', 'request_password_reset', 'complete_password_reset']: return [permissions.AllowAny()] return super().get_permissions() @action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny]) def signup(self, request): username = request.data.get('username') email = request.data.get('email') password = request.data.get('password') if not username or not email or not password: return Response({'error': 'Username, email and password required'}, status=status.HTTP_400_BAD_REQUEST) if User.objects.filter(username=username).exists(): return Response({'error': 'Username already exists'}, status=status.HTTP_400_BAD_REQUEST) if User.objects.filter(email=email).exists(): return Response({'error': 'Email already exists'}, status=status.HTTP_400_BAD_REQUEST) import secrets import string code = ''.join(secrets.choice(string.digits) for _ in range(6)) user = User.objects.create_user( username=username, email=email, password=password, is_active=False, email_confirmation_code=code ) try: send_mail( 'Verify your Holy Flavors account', f'Hi {username},\n\nYour verification code is: {code}', 'noreply@holyflavors.com', [email], fail_silently=False, ) except Exception as e: # If email fails for any reason, delete the user so they can try again user.delete() return Response( {'error': f'Failed to send verification email. Please check your SMTP settings in the .env file. The server error was: {e}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) return Response({'status': 'User created, please verify your email'}, status=status.HTTP_201_CREATED) @action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny]) def verify_signup(self, request): username = request.data.get('username') code = request.data.get('code') try: user = User.objects.get(username=username, is_active=False) except User.DoesNotExist: return Response({'error': 'User not found or already active'}, status=status.HTTP_404_NOT_FOUND) if user.email_confirmation_code == code: user.is_active = True user.email_confirmation_code = None user.save() return Response({'status': 'Account verified successfully!'}) else: return Response({'error': 'Invalid verification code'}, status=status.HTTP_400_BAD_REQUEST) @action(detail=False, methods=['get']) def me(self, request): serializer = self.get_serializer(request.user) return Response(serializer.data) @action(detail=False, methods=['patch']) def update_theme(self, request): user = request.user theme = request.data.get('theme') if theme in dict(User.THEME_CHOICES): user.theme = theme user.save() return Response({'status': 'theme updated', 'theme': theme}) return Response({'error': 'Invalid theme'}, status=status.HTTP_400_BAD_REQUEST) @action(detail=False, methods=['post']) def change_password(self, request): user = request.user old_password = request.data.get('old_password') new_password = request.data.get('new_password') if not user.check_password(old_password): return Response({'error': 'Wrong old password'}, status=status.HTTP_400_BAD_REQUEST) user.set_password(new_password) user.save() return Response({'status': 'password changed'}) @action(detail=False, methods=['patch']) def update_profile(self, request): user = request.user username = request.data.get('username') email = request.data.get('email') message = "" if username and username != user.username: if User.objects.filter(username=username).exists(): return Response({'error': 'Username already taken'}, status=status.HTTP_400_BAD_REQUEST) user.username = username if email and email != user.email: import secrets import string code = ''.join(secrets.choice(string.digits) for _ in range(6)) user.pending_email = email user.email_confirmation_code = code send_mail( 'Confirm your new email', f'Hi {user.username},\n\nYour confirmation code is: {code}', 'noreply@holyflavors.com', [email], fail_silently=False, ) message = " Confirmation code sent to new email." user.save() serializer = self.get_serializer(user) data = serializer.data if message: data['message'] = message return Response(data) @action(detail=False, methods=['post']) def confirm_email(self, request): user = request.user code = request.data.get('code') if not user.email_confirmation_code or code != user.email_confirmation_code: return Response({'error': 'Invalid or expired code'}, status=status.HTTP_400_BAD_REQUEST) user.email = user.pending_email user.pending_email = None user.email_confirmation_code = None user.save() return Response({'status': 'Email confirmed', 'email': user.email}) @action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny]) def request_password_reset(self, request): email = request.data.get('email') try: user = User.objects.get(email=email, is_active=True) except User.DoesNotExist: return Response({'status': 'If an account exists with this email, a reset code has been sent.'}) import secrets import string code = ''.join(secrets.choice(string.digits) for _ in range(6)) user.email_confirmation_code = code user.save() send_mail( 'Password Reset Request', f'Hi {user.username},\n\nYour password reset code is: {code}', 'noreply@holyflavors.com', [email], fail_silently=False, ) return Response({'status': 'If an account exists with this email, a reset code has been sent.'}) @action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny]) def complete_password_reset(self, request): email = request.data.get('email') code = request.data.get('code') new_password = request.data.get('password') try: user = User.objects.get(email=email, email_confirmation_code=code) except User.DoesNotExist: return Response({'error': 'Invalid email or code'}, status=status.HTTP_400_BAD_REQUEST) user.set_password(new_password) user.email_confirmation_code = None user.save() return Response({'status': 'Password reset successful!'}) @action(detail=False, methods=['get']) def dashboard(self, request): user = request.user rated_ids = user.ratings.values_list('flavor_id', flat=True) missing_flavors = Flavor.objects.exclude(id__in=rated_ids).select_related('category') rated_flavors = user.ratings.select_related('flavor', 'flavor__category') return Response({ 'user': UserSerializer(user).data, 'rated_count': rated_flavors.count(), 'missing_count': missing_flavors.count(), 'missing_flavors': FlavorSerializer(missing_flavors, many=True, context={'request': request}).data, 'my_ratings': RatingSerializer(rated_flavors, many=True).data })