from django.contrib.gis.geos import Point, Polygon from django.contrib.gis.measure import D from rest_framework import generics, status from rest_framework.response import Response from rest_framework.views import APIView from apps.common.permissions import IsCreator from apps.utils.fcm import send_notification from .models import Challenge, ChallengeParticipant from .serializers import ( ChallengeCreateSerializer, ChallengeUpdateSerializer, ChallengeDetailSerializer, ChallengeMapSerializer, ChallengeParticipantSerializer, ) def _parse_bbox(bbox_str): try: min_lon, min_lat, max_lon, max_lat = [float(x) for x in bbox_str.split(",")] poly = Polygon.from_bbox((min_lon, min_lat, max_lon, max_lat)) poly.srid = 4326 return poly, None except Exception: return None, "bbox must be four comma-separated floats: minLon,minLat,maxLon,maxLat" class ChallengeListCreateView(APIView): def get(self, request): qs = Challenge.objects.select_related("creator") # ?mine=true — return the authenticated user's own challenges, all statuses if request.query_params.get("mine") == "true": qs = qs.filter(creator=request.user) return Response( ChallengeDetailSerializer(qs, many=True, context={"request": request}).data ) status_filter = request.query_params.get("status", "active") qs = qs.filter(status=status_filter) bbox_str = request.query_params.get("bbox") if bbox_str: bbox, error = _parse_bbox(bbox_str) if error: return Response({"error": "invalid_bbox", "detail": error}, status=status.HTTP_400_BAD_REQUEST) qs = qs.filter(region_centroid__within=bbox) near_str = request.query_params.get("near") if near_str: try: lat, lon, radius_m = [float(x) for x in near_str.split(",")] except Exception: return Response( {"error": "invalid_near", "detail": "near must be lat,lon,radius_m"}, status=status.HTTP_400_BAD_REQUEST, ) center = Point(lon, lat, srid=4326) qs = qs.filter(region_centroid__distance_lte=(center, D(m=radius_m))) return Response(ChallengeMapSerializer(qs, many=True).data) def post(self, request): serializer = ChallengeCreateSerializer(data=request.data) serializer.is_valid(raise_exception=True) challenge = serializer.save(creator=request.user) return Response( ChallengeDetailSerializer(challenge, context={"request": request}).data, status=status.HTTP_201_CREATED, ) class ChallengeDetailView(APIView): def _get_challenge(self, pk): try: return Challenge.objects.select_related("creator").prefetch_related("participants").get(pk=pk) except Challenge.DoesNotExist: return None def get(self, request, pk): challenge = self._get_challenge(pk) if not challenge: return Response(status=status.HTTP_404_NOT_FOUND) return Response(ChallengeDetailSerializer(challenge, context={"request": request}).data) def patch(self, request, pk): challenge = self._get_challenge(pk) if not challenge: return Response(status=status.HTTP_404_NOT_FOUND) permission = IsCreator() if not permission.has_object_permission(request, self, challenge): return Response(status=status.HTTP_403_FORBIDDEN) serializer = ChallengeUpdateSerializer(challenge, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() return Response(ChallengeDetailSerializer(challenge, context={"request": request}).data) def delete(self, request, pk): challenge = self._get_challenge(pk) if not challenge: return Response(status=status.HTTP_404_NOT_FOUND) permission = IsCreator() if not permission.has_object_permission(request, self, challenge): return Response(status=status.HTTP_403_FORBIDDEN) challenge.status = Challenge.Status.CLOSED challenge.save(update_fields=["status", "updated_at"]) return Response(status=status.HTTP_204_NO_CONTENT) class ChallengeParticipateView(APIView): def post(self, request, pk): try: challenge = Challenge.objects.select_related("creator").get(pk=pk) except Challenge.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) if challenge.status != Challenge.Status.ACTIVE: return Response( {"error": "challenge_closed", "detail": "This challenge is no longer active."}, status=status.HTTP_403_FORBIDDEN, ) participant, created = ChallengeParticipant.objects.get_or_create( challenge=challenge, user=request.user, ) if created: # Notify the challenge creator send_notification( challenge.creator.fcm_token, title="Someone accepted your challenge!", body=f'A user is heading out for "{challenge.title}".', data={"challenge_id": str(challenge.id), "type": "challenge_accepted"}, ) response_status = status.HTTP_201_CREATED if created else status.HTTP_200_OK return Response(ChallengeParticipantSerializer(participant).data, status=response_status) def delete(self, request, pk): try: participant = ChallengeParticipant.objects.get( challenge_id=pk, user=request.user, ) except ChallengeParticipant.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) if participant.submitted_splat_id: return Response( {"error": "cannot_leave", "detail": "You cannot remove yourself after submitting a splat."}, status=status.HTTP_403_FORBIDDEN, ) participant.delete() return Response(status=status.HTTP_204_NO_CONTENT) class ChallengeSplatsView(generics.ListAPIView): from apps.splats.serializers import SplatMapSerializer serializer_class = SplatMapSerializer def get_queryset(self): from apps.splats.models import Splat return Splat.objects.filter( challenge_id=self.kwargs["pk"], is_published=True, ).order_by("-created_at")