171 lines
6.1 KiB
Python
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")
|