rcnn/backend/apps/challenges/views.py
Marius Unsel d93412cd0d Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 01:12:40 +02:00

171 lines
6.1 KiB
Python

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.all()
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")