mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-27 19:06:59 -05:00
feat: Add blog, media, and support apps, implement ride credits and image API, and remove toplist feature.
This commit is contained in:
88
backend/apps/api/v1/parks/history_views.py
Normal file
88
backend/apps/api/v1/parks/history_views.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
Park history API views.
|
||||
"""
|
||||
|
||||
from rest_framework import viewsets, mixins
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from apps.parks.models import Park
|
||||
from apps.rides.models import Ride
|
||||
from apps.api.v1.serializers.history import ParkHistoryOutputSerializer, RideHistoryOutputSerializer
|
||||
|
||||
class ParkHistoryViewSet(viewsets.GenericViewSet):
|
||||
"""
|
||||
ViewSet for retrieving park history.
|
||||
"""
|
||||
permission_classes = [AllowAny]
|
||||
lookup_field = "slug"
|
||||
lookup_url_kwarg = "park_slug"
|
||||
|
||||
@extend_schema(
|
||||
summary="Get park history",
|
||||
description="Retrieve history events for a park.",
|
||||
responses={200: ParkHistoryOutputSerializer},
|
||||
tags=["Park History"],
|
||||
)
|
||||
def list(self, request, park_slug=None):
|
||||
park = get_object_or_404(Park, slug=park_slug)
|
||||
|
||||
events = []
|
||||
if hasattr(park, "events"):
|
||||
events = park.events.all().order_by("-pgh_created_at")
|
||||
|
||||
summary = {
|
||||
"total_events": len(events),
|
||||
"first_recorded": events.last().pgh_created_at if len(events) else None,
|
||||
"last_modified": events.first().pgh_created_at if len(events) else None,
|
||||
}
|
||||
|
||||
data = {
|
||||
"park": park,
|
||||
"current_state": park,
|
||||
"summary": summary,
|
||||
"events": events
|
||||
}
|
||||
|
||||
serializer = ParkHistoryOutputSerializer(data)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class RideHistoryViewSet(viewsets.GenericViewSet):
|
||||
"""
|
||||
ViewSet for retrieving ride history.
|
||||
"""
|
||||
permission_classes = [AllowAny]
|
||||
lookup_field = "slug"
|
||||
lookup_url_kwarg = "ride_slug"
|
||||
|
||||
@extend_schema(
|
||||
summary="Get ride history",
|
||||
description="Retrieve history events for a ride.",
|
||||
responses={200: RideHistoryOutputSerializer},
|
||||
tags=["Ride History"],
|
||||
)
|
||||
def list(self, request, park_slug=None, ride_slug=None):
|
||||
park = get_object_or_404(Park, slug=park_slug)
|
||||
ride = get_object_or_404(Ride, slug=ride_slug, park=park)
|
||||
|
||||
events = []
|
||||
if hasattr(ride, "events"):
|
||||
events = ride.events.all().order_by("-pgh_created_at")
|
||||
|
||||
summary = {
|
||||
"total_events": len(events),
|
||||
"first_recorded": events.last().pgh_created_at if len(events) else None,
|
||||
"last_modified": events.first().pgh_created_at if len(events) else None,
|
||||
}
|
||||
|
||||
data = {
|
||||
"ride": ride,
|
||||
"current_state": ride,
|
||||
"summary": summary,
|
||||
"events": events
|
||||
}
|
||||
|
||||
serializer = RideHistoryOutputSerializer(data)
|
||||
return Response(serializer.data)
|
||||
162
backend/apps/api/v1/parks/park_reviews_views.py
Normal file
162
backend/apps/api/v1/parks/park_reviews_views.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
Park review API views for ThrillWiki API v1.
|
||||
|
||||
This module contains park review ViewSet following the reviews pattern.
|
||||
Provides CRUD operations for park reviews nested under parks/{slug}/reviews/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Avg
|
||||
from django.utils import timezone
|
||||
from drf_spectacular.utils import extend_schema_view, extend_schema
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError, NotFound
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from apps.parks.models import Park, ParkReview
|
||||
from apps.api.v1.serializers.park_reviews import (
|
||||
ParkReviewOutputSerializer,
|
||||
ParkReviewCreateInputSerializer,
|
||||
ParkReviewUpdateInputSerializer,
|
||||
ParkReviewListOutputSerializer,
|
||||
ParkReviewStatsOutputSerializer,
|
||||
ParkReviewModerationInputSerializer,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
summary="List park reviews",
|
||||
tags=["Park Reviews"],
|
||||
),
|
||||
create=extend_schema(
|
||||
summary="Create park review",
|
||||
tags=["Park Reviews"],
|
||||
),
|
||||
retrieve=extend_schema(
|
||||
summary="Get park review details",
|
||||
tags=["Park Reviews"],
|
||||
),
|
||||
update=extend_schema(
|
||||
summary="Update park review",
|
||||
tags=["Park Reviews"],
|
||||
),
|
||||
partial_update=extend_schema(
|
||||
summary="Partially update park review",
|
||||
tags=["Park Reviews"],
|
||||
),
|
||||
destroy=extend_schema(
|
||||
summary="Delete park review",
|
||||
tags=["Park Reviews"],
|
||||
),
|
||||
)
|
||||
class ParkReviewViewSet(ModelViewSet):
|
||||
"""
|
||||
ViewSet for managing park reviews with full CRUD operations.
|
||||
"""
|
||||
|
||||
lookup_field = "id"
|
||||
|
||||
def get_permissions(self):
|
||||
"""Set permissions based on action."""
|
||||
if self.action in ['list', 'retrieve', 'stats']:
|
||||
permission_classes = [AllowAny]
|
||||
else:
|
||||
permission_classes = [IsAuthenticated]
|
||||
return [permission() for permission in permission_classes]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get reviews for the current park."""
|
||||
queryset = ParkReview.objects.select_related(
|
||||
"park", "user", "user__profile"
|
||||
)
|
||||
|
||||
park_slug = self.kwargs.get("park_slug")
|
||||
if park_slug:
|
||||
try:
|
||||
park, _ = Park.get_by_slug(park_slug)
|
||||
queryset = queryset.filter(park=park)
|
||||
except Park.DoesNotExist:
|
||||
return queryset.none()
|
||||
|
||||
if not (hasattr(self.request, 'user') and getattr(self.request.user, 'is_staff', False)):
|
||||
queryset = queryset.filter(is_published=True)
|
||||
|
||||
return queryset.order_by("-created_at")
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "list":
|
||||
return ParkReviewListOutputSerializer
|
||||
elif self.action == "create":
|
||||
return ParkReviewCreateInputSerializer
|
||||
elif self.action in ["update", "partial_update"]:
|
||||
return ParkReviewUpdateInputSerializer
|
||||
else:
|
||||
return ParkReviewOutputSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
park_slug = self.kwargs.get("park_slug")
|
||||
try:
|
||||
park, _ = Park.get_by_slug(park_slug)
|
||||
except Park.DoesNotExist:
|
||||
raise NotFound("Park not found")
|
||||
|
||||
if ParkReview.objects.filter(park=park, user=self.request.user).exists():
|
||||
raise ValidationError("You have already reviewed this park")
|
||||
|
||||
serializer.save(
|
||||
park=park,
|
||||
user=self.request.user,
|
||||
is_published=True
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
instance = self.get_object()
|
||||
if not (self.request.user == instance.user or getattr(self.request.user, "is_staff", False)):
|
||||
raise PermissionDenied("You can only edit your own reviews.")
|
||||
serializer.save()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not (self.request.user == instance.user or getattr(self.request.user, "is_staff", False)):
|
||||
raise PermissionDenied("You can only delete your own reviews.")
|
||||
instance.delete()
|
||||
|
||||
@extend_schema(
|
||||
summary="Get park review statistics",
|
||||
responses={200: ParkReviewStatsOutputSerializer},
|
||||
tags=["Park Reviews"],
|
||||
)
|
||||
@action(detail=False, methods=["get"])
|
||||
def stats(self, request, park_slug=None):
|
||||
try:
|
||||
park, _ = Park.get_by_slug(park_slug)
|
||||
except Park.DoesNotExist:
|
||||
return Response({"error": "Park not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
reviews = ParkReview.objects.filter(park=park, is_published=True)
|
||||
total_reviews = reviews.count()
|
||||
avg_rating = reviews.aggregate(avg=Avg('rating'))['avg']
|
||||
|
||||
rating_distribution = {}
|
||||
for i in range(1, 11):
|
||||
rating_distribution[str(i)] = reviews.filter(rating=i).count()
|
||||
|
||||
from datetime import timedelta
|
||||
recent_reviews = reviews.filter(created_at__gte=timezone.now() - timedelta(days=30)).count()
|
||||
|
||||
stats = {
|
||||
"total_reviews": total_reviews,
|
||||
"published_reviews": total_reviews,
|
||||
"pending_reviews": ParkReview.objects.filter(park=park, is_published=False).count(),
|
||||
"average_rating": avg_rating,
|
||||
"rating_distribution": rating_distribution,
|
||||
"recent_reviews": recent_reviews,
|
||||
}
|
||||
return Response(ParkReviewStatsOutputSerializer(stats).data)
|
||||
@@ -42,10 +42,19 @@ router.register(r"", ParkPhotoViewSet, basename="park-photo")
|
||||
# Create routers for nested ride endpoints
|
||||
ride_photos_router = DefaultRouter()
|
||||
ride_photos_router.register(r"", RidePhotoViewSet, basename="ride-photo")
|
||||
from .ride_reviews_views import RideReviewViewSet
|
||||
|
||||
ride_reviews_router = DefaultRouter()
|
||||
ride_reviews_router.register(r"", RideReviewViewSet, basename="ride-review")
|
||||
|
||||
from .park_reviews_views import ParkReviewViewSet
|
||||
from .history_views import ParkHistoryViewSet, RideHistoryViewSet
|
||||
|
||||
# Create routers for nested park endpoints
|
||||
reviews_router = DefaultRouter()
|
||||
reviews_router.register(r"", ParkReviewViewSet, basename="park-review")
|
||||
|
||||
|
||||
app_name = "api_v1_parks"
|
||||
|
||||
urlpatterns = [
|
||||
@@ -86,7 +95,7 @@ urlpatterns = [
|
||||
name="park-image-settings",
|
||||
),
|
||||
# Park photo endpoints - domain-specific photo management
|
||||
path("<int:park_pk>/photos/", include(router.urls)),
|
||||
path("<str:park_pk>/photos/", include(router.urls)),
|
||||
|
||||
# Nested ride photo endpoints - photos for specific rides within parks
|
||||
path("<str:park_slug>/rides/<str:ride_slug>/photos/", include(ride_photos_router.urls)),
|
||||
@@ -95,6 +104,15 @@ urlpatterns = [
|
||||
path("<str:park_slug>/rides/<str:ride_slug>/reviews/", include(ride_reviews_router.urls)),
|
||||
# Nested ride review endpoints - reviews for specific rides within parks
|
||||
path("<str:park_slug>/rides/<str:ride_slug>/reviews/", include(ride_reviews_router.urls)),
|
||||
|
||||
# Ride History
|
||||
path("<str:park_slug>/rides/<str:ride_slug>/history/", RideHistoryViewSet.as_view({'get': 'list'}), name="ride-history"),
|
||||
|
||||
# Park Reviews
|
||||
path("<str:park_slug>/reviews/", include(reviews_router.urls)),
|
||||
|
||||
# Park History
|
||||
path("<str:park_slug>/history/", ParkHistoryViewSet.as_view({'get': 'list'}), name="park-history"),
|
||||
|
||||
# Roadtrip API endpoints
|
||||
path("roadtrip/create/", CreateTripView.as_view(), name="roadtrip-create"),
|
||||
|
||||
@@ -142,10 +142,14 @@ class ParkPhotoViewSet(ModelViewSet):
|
||||
"park", "park__operator", "uploaded_by"
|
||||
)
|
||||
|
||||
# If park_pk is provided in URL kwargs, filter by park
|
||||
# If park_pk is provided in URL kwargs, filter by park
|
||||
park_pk = self.kwargs.get("park_pk")
|
||||
if park_pk:
|
||||
queryset = queryset.filter(park_id=park_pk)
|
||||
if str(park_pk).isdigit():
|
||||
queryset = queryset.filter(park_id=park_pk)
|
||||
else:
|
||||
queryset = queryset.filter(park__slug=park_pk)
|
||||
|
||||
return queryset.order_by("-created_at")
|
||||
|
||||
@@ -164,10 +168,16 @@ class ParkPhotoViewSet(ModelViewSet):
|
||||
"""Create a new park photo using ParkMediaService."""
|
||||
park_id = self.kwargs.get("park_pk")
|
||||
if not park_id:
|
||||
raise ValidationError("Park ID is required")
|
||||
raise ValidationError("Park ID/Slug is required")
|
||||
|
||||
try:
|
||||
Park.objects.get(pk=park_id)
|
||||
if str(park_id).isdigit():
|
||||
park = Park.objects.get(pk=park_id)
|
||||
else:
|
||||
park = Park.objects.get(slug=park_id)
|
||||
|
||||
# Use real park ID
|
||||
park_id = park.id
|
||||
except Park.DoesNotExist:
|
||||
raise ValidationError("Park not found")
|
||||
|
||||
@@ -342,7 +352,10 @@ class ParkPhotoViewSet(ModelViewSet):
|
||||
# Filter photos to only those belonging to this park (if park_pk provided)
|
||||
photos_queryset = ParkPhoto.objects.filter(id__in=photo_ids)
|
||||
if park_id:
|
||||
photos_queryset = photos_queryset.filter(park_id=park_id)
|
||||
if str(park_id).isdigit():
|
||||
photos_queryset = photos_queryset.filter(park_id=park_id)
|
||||
else:
|
||||
photos_queryset = photos_queryset.filter(park__slug=park_id)
|
||||
|
||||
updated_count = photos_queryset.update(is_approved=approve)
|
||||
|
||||
@@ -385,10 +398,13 @@ class ParkPhotoViewSet(ModelViewSet):
|
||||
park = None
|
||||
if park_pk:
|
||||
try:
|
||||
park = Park.objects.get(pk=park_pk)
|
||||
if str(park_pk).isdigit():
|
||||
park = Park.objects.get(pk=park_pk)
|
||||
else:
|
||||
park = Park.objects.get(slug=park_pk)
|
||||
except Park.DoesNotExist:
|
||||
return ErrorHandler.handle_api_error(
|
||||
NotFoundError(f"Park with id {park_pk} not found"),
|
||||
NotFoundError(f"Park with id/slug {park_pk} not found"),
|
||||
user_message="Park not found",
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
@@ -474,7 +490,10 @@ class ParkPhotoViewSet(ModelViewSet):
|
||||
)
|
||||
|
||||
try:
|
||||
park = Park.objects.get(pk=park_pk)
|
||||
if str(park_pk).isdigit():
|
||||
park = Park.objects.get(pk=park_pk)
|
||||
else:
|
||||
park = Park.objects.get(slug=park_pk)
|
||||
except Park.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Park not found"},
|
||||
|
||||
Reference in New Issue
Block a user