mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-01-01 23:27:02 -05:00
feat: Implement initial schema and add various API, service, and management command enhancements across the application.
This commit is contained in:
@@ -24,6 +24,7 @@ from apps.api.v1.serializers.companies import (
|
||||
|
||||
try:
|
||||
from apps.rides.models.company import Company
|
||||
|
||||
MODELS_AVAILABLE = True
|
||||
except ImportError:
|
||||
Company = None
|
||||
@@ -65,9 +66,7 @@ class CompanyListCreateAPIView(APIView):
|
||||
# Search filter
|
||||
search = request.query_params.get("search", "")
|
||||
if search:
|
||||
qs = qs.filter(
|
||||
Q(name__icontains=search) | Q(description__icontains=search)
|
||||
)
|
||||
qs = qs.filter(Q(name__icontains=search) | Q(description__icontains=search))
|
||||
|
||||
# Role filter
|
||||
role = request.query_params.get("role", "")
|
||||
@@ -120,7 +119,7 @@ class CompanyDetailAPIView(APIView):
|
||||
try:
|
||||
return Company.objects.get(pk=pk)
|
||||
except Company.DoesNotExist:
|
||||
raise NotFound("Company not found")
|
||||
raise NotFound("Company not found") from None
|
||||
|
||||
@extend_schema(
|
||||
summary="Retrieve a company",
|
||||
|
||||
@@ -93,18 +93,10 @@ class RideModelListCreateAPIView(APIView):
|
||||
type=OpenApiTypes.STR,
|
||||
required=True,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="page_size", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="search", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="category", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
||||
),
|
||||
OpenApiParameter(name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT),
|
||||
OpenApiParameter(name="page_size", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT),
|
||||
OpenApiParameter(name="search", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR),
|
||||
OpenApiParameter(name="category", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR),
|
||||
OpenApiParameter(
|
||||
name="target_market",
|
||||
location=OpenApiParameter.QUERY,
|
||||
@@ -134,7 +126,7 @@ class RideModelListCreateAPIView(APIView):
|
||||
try:
|
||||
manufacturer = Company.objects.get(slug=manufacturer_slug)
|
||||
except Company.DoesNotExist:
|
||||
raise NotFound("Manufacturer not found")
|
||||
raise NotFound("Manufacturer not found") from None
|
||||
|
||||
qs = (
|
||||
RideModel.objects.filter(manufacturer=manufacturer)
|
||||
@@ -176,13 +168,9 @@ class RideModelListCreateAPIView(APIView):
|
||||
|
||||
# Year filters
|
||||
if filters.get("first_installation_year_min"):
|
||||
qs = qs.filter(
|
||||
first_installation_year__gte=filters["first_installation_year_min"]
|
||||
)
|
||||
qs = qs.filter(first_installation_year__gte=filters["first_installation_year_min"])
|
||||
if filters.get("first_installation_year_max"):
|
||||
qs = qs.filter(
|
||||
first_installation_year__lte=filters["first_installation_year_max"]
|
||||
)
|
||||
qs = qs.filter(first_installation_year__lte=filters["first_installation_year_max"])
|
||||
|
||||
# Installation count filter
|
||||
if filters.get("min_installations"):
|
||||
@@ -190,23 +178,15 @@ class RideModelListCreateAPIView(APIView):
|
||||
|
||||
# Height filters
|
||||
if filters.get("min_height_ft"):
|
||||
qs = qs.filter(
|
||||
typical_height_range_max_ft__gte=filters["min_height_ft"]
|
||||
)
|
||||
qs = qs.filter(typical_height_range_max_ft__gte=filters["min_height_ft"])
|
||||
if filters.get("max_height_ft"):
|
||||
qs = qs.filter(
|
||||
typical_height_range_min_ft__lte=filters["max_height_ft"]
|
||||
)
|
||||
qs = qs.filter(typical_height_range_min_ft__lte=filters["max_height_ft"])
|
||||
|
||||
# Speed filters
|
||||
if filters.get("min_speed_mph"):
|
||||
qs = qs.filter(
|
||||
typical_speed_range_max_mph__gte=filters["min_speed_mph"]
|
||||
)
|
||||
qs = qs.filter(typical_speed_range_max_mph__gte=filters["min_speed_mph"])
|
||||
if filters.get("max_speed_mph"):
|
||||
qs = qs.filter(
|
||||
typical_speed_range_min_mph__lte=filters["max_speed_mph"]
|
||||
)
|
||||
qs = qs.filter(typical_speed_range_min_mph__lte=filters["max_speed_mph"])
|
||||
|
||||
# Ordering
|
||||
ordering = filters.get("ordering", "manufacturer__name,name")
|
||||
@@ -216,9 +196,7 @@ class RideModelListCreateAPIView(APIView):
|
||||
|
||||
paginator = StandardResultsSetPagination()
|
||||
page = paginator.paginate_queryset(qs, request)
|
||||
serializer = RideModelListOutputSerializer(
|
||||
page, many=True, context={"request": request}
|
||||
)
|
||||
serializer = RideModelListOutputSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
@extend_schema(
|
||||
@@ -240,9 +218,7 @@ class RideModelListCreateAPIView(APIView):
|
||||
"""Create a new ride model for a specific manufacturer."""
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{
|
||||
"detail": "Ride model creation is not available because domain models are not imported."
|
||||
},
|
||||
{"detail": "Ride model creation is not available because domain models are not imported."},
|
||||
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
)
|
||||
|
||||
@@ -250,7 +226,7 @@ class RideModelListCreateAPIView(APIView):
|
||||
try:
|
||||
manufacturer = Company.objects.get(slug=manufacturer_slug)
|
||||
except Company.DoesNotExist:
|
||||
raise NotFound("Manufacturer not found")
|
||||
raise NotFound("Manufacturer not found") from None
|
||||
|
||||
serializer_in = RideModelCreateInputSerializer(data=request.data)
|
||||
serializer_in.is_valid(raise_exception=True)
|
||||
@@ -279,18 +255,14 @@ class RideModelListCreateAPIView(APIView):
|
||||
target_market=validated.get("target_market", ""),
|
||||
)
|
||||
|
||||
out_serializer = RideModelDetailOutputSerializer(
|
||||
ride_model, context={"request": request}
|
||||
)
|
||||
out_serializer = RideModelDetailOutputSerializer(ride_model, context={"request": request})
|
||||
return Response(out_serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class RideModelDetailAPIView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def _get_ride_model_or_404(
|
||||
self, manufacturer_slug: str, ride_model_slug: str
|
||||
) -> Any:
|
||||
def _get_ride_model_or_404(self, manufacturer_slug: str, ride_model_slug: str) -> Any:
|
||||
if not MODELS_AVAILABLE:
|
||||
raise NotFound("Ride model models not available")
|
||||
try:
|
||||
@@ -300,7 +272,7 @@ class RideModelDetailAPIView(APIView):
|
||||
.get(manufacturer__slug=manufacturer_slug, slug=ride_model_slug)
|
||||
)
|
||||
except RideModel.DoesNotExist:
|
||||
raise NotFound("Ride model not found")
|
||||
raise NotFound("Ride model not found") from None
|
||||
|
||||
@extend_schema(
|
||||
summary="Retrieve a ride model",
|
||||
@@ -322,13 +294,9 @@ class RideModelDetailAPIView(APIView):
|
||||
responses={200: RideModelDetailOutputSerializer()},
|
||||
tags=["Ride Models"],
|
||||
)
|
||||
def get(
|
||||
self, request: Request, manufacturer_slug: str, ride_model_slug: str
|
||||
) -> Response:
|
||||
def get(self, request: Request, manufacturer_slug: str, ride_model_slug: str) -> Response:
|
||||
ride_model = self._get_ride_model_or_404(manufacturer_slug, ride_model_slug)
|
||||
serializer = RideModelDetailOutputSerializer(
|
||||
ride_model, context={"request": request}
|
||||
)
|
||||
serializer = RideModelDetailOutputSerializer(ride_model, context={"request": request})
|
||||
return Response(serializer.data)
|
||||
|
||||
@extend_schema(
|
||||
@@ -352,9 +320,7 @@ class RideModelDetailAPIView(APIView):
|
||||
responses={200: RideModelDetailOutputSerializer()},
|
||||
tags=["Ride Models"],
|
||||
)
|
||||
def patch(
|
||||
self, request: Request, manufacturer_slug: str, ride_model_slug: str
|
||||
) -> Response:
|
||||
def patch(self, request: Request, manufacturer_slug: str, ride_model_slug: str) -> Response:
|
||||
ride_model = self._get_ride_model_or_404(manufacturer_slug, ride_model_slug)
|
||||
serializer_in = RideModelUpdateInputSerializer(data=request.data, partial=True)
|
||||
serializer_in.is_valid(raise_exception=True)
|
||||
@@ -366,20 +332,16 @@ class RideModelDetailAPIView(APIView):
|
||||
manufacturer = Company.objects.get(id=value)
|
||||
ride_model.manufacturer = manufacturer
|
||||
except Company.DoesNotExist:
|
||||
raise ValidationError({"manufacturer_id": "Manufacturer not found"})
|
||||
raise ValidationError({"manufacturer_id": "Manufacturer not found"}) from None
|
||||
else:
|
||||
setattr(ride_model, field, value)
|
||||
|
||||
ride_model.save()
|
||||
|
||||
serializer = RideModelDetailOutputSerializer(
|
||||
ride_model, context={"request": request}
|
||||
)
|
||||
serializer = RideModelDetailOutputSerializer(ride_model, context={"request": request})
|
||||
return Response(serializer.data)
|
||||
|
||||
def put(
|
||||
self, request: Request, manufacturer_slug: str, ride_model_slug: str
|
||||
) -> Response:
|
||||
def put(self, request: Request, manufacturer_slug: str, ride_model_slug: str) -> Response:
|
||||
# Full replace - reuse patch behavior for simplicity
|
||||
return self.patch(request, manufacturer_slug, ride_model_slug)
|
||||
|
||||
@@ -403,9 +365,7 @@ class RideModelDetailAPIView(APIView):
|
||||
responses={204: None},
|
||||
tags=["Ride Models"],
|
||||
)
|
||||
def delete(
|
||||
self, request: Request, manufacturer_slug: str, ride_model_slug: str
|
||||
) -> Response:
|
||||
def delete(self, request: Request, manufacturer_slug: str, ride_model_slug: str) -> Response:
|
||||
ride_model = self._get_ride_model_or_404(manufacturer_slug, ride_model_slug)
|
||||
ride_model.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -449,9 +409,7 @@ class RideModelSearchAPIView(APIView):
|
||||
)
|
||||
|
||||
qs = RideModel.objects.filter(
|
||||
Q(name__icontains=q)
|
||||
| Q(description__icontains=q)
|
||||
| Q(manufacturer__name__icontains=q)
|
||||
Q(name__icontains=q) | Q(description__icontains=q) | Q(manufacturer__name__icontains=q)
|
||||
).select_related("manufacturer")[:20]
|
||||
|
||||
results = [
|
||||
@@ -491,8 +449,8 @@ class RideModelFilterOptionsAPIView(APIView):
|
||||
# Use Rich Choice Objects for fallback options
|
||||
try:
|
||||
# Get rich choice objects from registry
|
||||
categories = get_choices('categories', 'rides')
|
||||
target_markets = get_choices('target_markets', 'rides')
|
||||
categories = get_choices("categories", "rides")
|
||||
target_markets = get_choices("target_markets", "rides")
|
||||
|
||||
# Convert Rich Choice Objects to frontend format with metadata
|
||||
categories_data = [
|
||||
@@ -500,10 +458,10 @@ class RideModelFilterOptionsAPIView(APIView):
|
||||
"value": choice.value,
|
||||
"label": choice.label,
|
||||
"description": choice.description,
|
||||
"color": choice.metadata.get('color'),
|
||||
"icon": choice.metadata.get('icon'),
|
||||
"css_class": choice.metadata.get('css_class'),
|
||||
"sort_order": choice.metadata.get('sort_order', 0)
|
||||
"color": choice.metadata.get("color"),
|
||||
"icon": choice.metadata.get("icon"),
|
||||
"css_class": choice.metadata.get("css_class"),
|
||||
"sort_order": choice.metadata.get("sort_order", 0),
|
||||
}
|
||||
for choice in categories
|
||||
]
|
||||
@@ -513,10 +471,10 @@ class RideModelFilterOptionsAPIView(APIView):
|
||||
"value": choice.value,
|
||||
"label": choice.label,
|
||||
"description": choice.description,
|
||||
"color": choice.metadata.get('color'),
|
||||
"icon": choice.metadata.get('icon'),
|
||||
"css_class": choice.metadata.get('css_class'),
|
||||
"sort_order": choice.metadata.get('sort_order', 0)
|
||||
"color": choice.metadata.get("color"),
|
||||
"icon": choice.metadata.get("icon"),
|
||||
"css_class": choice.metadata.get("css_class"),
|
||||
"sort_order": choice.metadata.get("sort_order", 0),
|
||||
}
|
||||
for choice in target_markets
|
||||
]
|
||||
@@ -524,25 +482,173 @@ class RideModelFilterOptionsAPIView(APIView):
|
||||
except Exception:
|
||||
# Ultimate fallback with basic structure
|
||||
categories_data = [
|
||||
{"value": "RC", "label": "Roller Coaster", "description": "High-speed thrill rides with tracks", "color": "red", "icon": "roller-coaster", "css_class": "bg-red-100 text-red-800", "sort_order": 1},
|
||||
{"value": "DR", "label": "Dark Ride", "description": "Indoor themed experiences", "color": "purple", "icon": "dark-ride", "css_class": "bg-purple-100 text-purple-800", "sort_order": 2},
|
||||
{"value": "FR", "label": "Flat Ride", "description": "Spinning and rotating attractions", "color": "blue", "icon": "flat-ride", "css_class": "bg-blue-100 text-blue-800", "sort_order": 3},
|
||||
{"value": "WR", "label": "Water Ride", "description": "Water-based attractions and slides", "color": "cyan", "icon": "water-ride", "css_class": "bg-cyan-100 text-cyan-800", "sort_order": 4},
|
||||
{"value": "TR", "label": "Transport", "description": "Transportation systems within parks", "color": "green", "icon": "transport", "css_class": "bg-green-100 text-green-800", "sort_order": 5},
|
||||
{"value": "OT", "label": "Other", "description": "Miscellaneous attractions", "color": "gray", "icon": "other", "css_class": "bg-gray-100 text-gray-800", "sort_order": 6},
|
||||
{
|
||||
"value": "RC",
|
||||
"label": "Roller Coaster",
|
||||
"description": "High-speed thrill rides with tracks",
|
||||
"color": "red",
|
||||
"icon": "roller-coaster",
|
||||
"css_class": "bg-red-100 text-red-800",
|
||||
"sort_order": 1,
|
||||
},
|
||||
{
|
||||
"value": "DR",
|
||||
"label": "Dark Ride",
|
||||
"description": "Indoor themed experiences",
|
||||
"color": "purple",
|
||||
"icon": "dark-ride",
|
||||
"css_class": "bg-purple-100 text-purple-800",
|
||||
"sort_order": 2,
|
||||
},
|
||||
{
|
||||
"value": "FR",
|
||||
"label": "Flat Ride",
|
||||
"description": "Spinning and rotating attractions",
|
||||
"color": "blue",
|
||||
"icon": "flat-ride",
|
||||
"css_class": "bg-blue-100 text-blue-800",
|
||||
"sort_order": 3,
|
||||
},
|
||||
{
|
||||
"value": "WR",
|
||||
"label": "Water Ride",
|
||||
"description": "Water-based attractions and slides",
|
||||
"color": "cyan",
|
||||
"icon": "water-ride",
|
||||
"css_class": "bg-cyan-100 text-cyan-800",
|
||||
"sort_order": 4,
|
||||
},
|
||||
{
|
||||
"value": "TR",
|
||||
"label": "Transport",
|
||||
"description": "Transportation systems within parks",
|
||||
"color": "green",
|
||||
"icon": "transport",
|
||||
"css_class": "bg-green-100 text-green-800",
|
||||
"sort_order": 5,
|
||||
},
|
||||
{
|
||||
"value": "OT",
|
||||
"label": "Other",
|
||||
"description": "Miscellaneous attractions",
|
||||
"color": "gray",
|
||||
"icon": "other",
|
||||
"css_class": "bg-gray-100 text-gray-800",
|
||||
"sort_order": 6,
|
||||
},
|
||||
]
|
||||
target_markets_data = [
|
||||
{"value": "FAMILY", "label": "Family", "description": "Suitable for all family members", "color": "green", "icon": "family", "css_class": "bg-green-100 text-green-800", "sort_order": 1},
|
||||
{"value": "THRILL", "label": "Thrill", "description": "High-intensity thrill experience", "color": "orange", "icon": "thrill", "css_class": "bg-orange-100 text-orange-800", "sort_order": 2},
|
||||
{"value": "EXTREME", "label": "Extreme", "description": "Maximum intensity experience", "color": "red", "icon": "extreme", "css_class": "bg-red-100 text-red-800", "sort_order": 3},
|
||||
{"value": "KIDDIE", "label": "Kiddie", "description": "Designed for young children", "color": "pink", "icon": "kiddie", "css_class": "bg-pink-100 text-pink-800", "sort_order": 4},
|
||||
{"value": "ALL_AGES", "label": "All Ages", "description": "Enjoyable for all age groups", "color": "blue", "icon": "all-ages", "css_class": "bg-blue-100 text-blue-800", "sort_order": 5},
|
||||
{
|
||||
"value": "FAMILY",
|
||||
"label": "Family",
|
||||
"description": "Suitable for all family members",
|
||||
"color": "green",
|
||||
"icon": "family",
|
||||
"css_class": "bg-green-100 text-green-800",
|
||||
"sort_order": 1,
|
||||
},
|
||||
{
|
||||
"value": "THRILL",
|
||||
"label": "Thrill",
|
||||
"description": "High-intensity thrill experience",
|
||||
"color": "orange",
|
||||
"icon": "thrill",
|
||||
"css_class": "bg-orange-100 text-orange-800",
|
||||
"sort_order": 2,
|
||||
},
|
||||
{
|
||||
"value": "EXTREME",
|
||||
"label": "Extreme",
|
||||
"description": "Maximum intensity experience",
|
||||
"color": "red",
|
||||
"icon": "extreme",
|
||||
"css_class": "bg-red-100 text-red-800",
|
||||
"sort_order": 3,
|
||||
},
|
||||
{
|
||||
"value": "KIDDIE",
|
||||
"label": "Kiddie",
|
||||
"description": "Designed for young children",
|
||||
"color": "pink",
|
||||
"icon": "kiddie",
|
||||
"css_class": "bg-pink-100 text-pink-800",
|
||||
"sort_order": 4,
|
||||
},
|
||||
{
|
||||
"value": "ALL_AGES",
|
||||
"label": "All Ages",
|
||||
"description": "Enjoyable for all age groups",
|
||||
"color": "blue",
|
||||
"icon": "all-ages",
|
||||
"css_class": "bg-blue-100 text-blue-800",
|
||||
"sort_order": 5,
|
||||
},
|
||||
]
|
||||
|
||||
return Response({
|
||||
return Response(
|
||||
{
|
||||
"categories": categories_data,
|
||||
"target_markets": target_markets_data,
|
||||
"manufacturers": [{"id": 1, "name": "Bolliger & Mabillard", "slug": "bolliger-mabillard"}],
|
||||
"ordering_options": [
|
||||
{"value": "name", "label": "Name A-Z"},
|
||||
{"value": "-name", "label": "Name Z-A"},
|
||||
{"value": "manufacturer__name", "label": "Manufacturer A-Z"},
|
||||
{"value": "-manufacturer__name", "label": "Manufacturer Z-A"},
|
||||
{"value": "first_installation_year", "label": "Oldest First"},
|
||||
{"value": "-first_installation_year", "label": "Newest First"},
|
||||
{"value": "total_installations", "label": "Fewest Installations"},
|
||||
{"value": "-total_installations", "label": "Most Installations"},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
# Get static choice definitions from Rich Choice Objects (primary source)
|
||||
# Get dynamic data from database queries
|
||||
|
||||
# Get rich choice objects from registry
|
||||
categories = get_choices("categories", "rides")
|
||||
target_markets = get_choices("target_markets", "rides")
|
||||
|
||||
# Convert Rich Choice Objects to frontend format with metadata
|
||||
categories_data = [
|
||||
{
|
||||
"value": choice.value,
|
||||
"label": choice.label,
|
||||
"description": choice.description,
|
||||
"color": choice.metadata.get("color"),
|
||||
"icon": choice.metadata.get("icon"),
|
||||
"css_class": choice.metadata.get("css_class"),
|
||||
"sort_order": choice.metadata.get("sort_order", 0),
|
||||
}
|
||||
for choice in categories
|
||||
]
|
||||
|
||||
target_markets_data = [
|
||||
{
|
||||
"value": choice.value,
|
||||
"label": choice.label,
|
||||
"description": choice.description,
|
||||
"color": choice.metadata.get("color"),
|
||||
"icon": choice.metadata.get("icon"),
|
||||
"css_class": choice.metadata.get("css_class"),
|
||||
"sort_order": choice.metadata.get("sort_order", 0),
|
||||
}
|
||||
for choice in target_markets
|
||||
]
|
||||
|
||||
# Get actual data from database
|
||||
manufacturers = (
|
||||
Company.objects.filter(roles__contains=["MANUFACTURER"], ride_models__isnull=False)
|
||||
.distinct()
|
||||
.values("id", "name", "slug")
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"categories": categories_data,
|
||||
"target_markets": target_markets_data,
|
||||
"manufacturers": [{"id": 1, "name": "Bolliger & Mabillard", "slug": "bolliger-mabillard"}],
|
||||
"manufacturers": list(manufacturers),
|
||||
"ordering_options": [
|
||||
{"value": "name", "label": "Name A-Z"},
|
||||
{"value": "-name", "label": "Name Z-A"},
|
||||
@@ -553,68 +659,9 @@ class RideModelFilterOptionsAPIView(APIView):
|
||||
{"value": "total_installations", "label": "Fewest Installations"},
|
||||
{"value": "-total_installations", "label": "Most Installations"},
|
||||
],
|
||||
})
|
||||
|
||||
# Get static choice definitions from Rich Choice Objects (primary source)
|
||||
# Get dynamic data from database queries
|
||||
|
||||
# Get rich choice objects from registry
|
||||
categories = get_choices('categories', 'rides')
|
||||
target_markets = get_choices('target_markets', 'rides')
|
||||
|
||||
# Convert Rich Choice Objects to frontend format with metadata
|
||||
categories_data = [
|
||||
{
|
||||
"value": choice.value,
|
||||
"label": choice.label,
|
||||
"description": choice.description,
|
||||
"color": choice.metadata.get('color'),
|
||||
"icon": choice.metadata.get('icon'),
|
||||
"css_class": choice.metadata.get('css_class'),
|
||||
"sort_order": choice.metadata.get('sort_order', 0)
|
||||
}
|
||||
for choice in categories
|
||||
]
|
||||
|
||||
target_markets_data = [
|
||||
{
|
||||
"value": choice.value,
|
||||
"label": choice.label,
|
||||
"description": choice.description,
|
||||
"color": choice.metadata.get('color'),
|
||||
"icon": choice.metadata.get('icon'),
|
||||
"css_class": choice.metadata.get('css_class'),
|
||||
"sort_order": choice.metadata.get('sort_order', 0)
|
||||
}
|
||||
for choice in target_markets
|
||||
]
|
||||
|
||||
# Get actual data from database
|
||||
manufacturers = (
|
||||
Company.objects.filter(
|
||||
roles__contains=["MANUFACTURER"], ride_models__isnull=False
|
||||
)
|
||||
.distinct()
|
||||
.values("id", "name", "slug")
|
||||
)
|
||||
|
||||
return Response({
|
||||
"categories": categories_data,
|
||||
"target_markets": target_markets_data,
|
||||
"manufacturers": list(manufacturers),
|
||||
"ordering_options": [
|
||||
{"value": "name", "label": "Name A-Z"},
|
||||
{"value": "-name", "label": "Name Z-A"},
|
||||
{"value": "manufacturer__name", "label": "Manufacturer A-Z"},
|
||||
{"value": "-manufacturer__name", "label": "Manufacturer Z-A"},
|
||||
{"value": "first_installation_year", "label": "Oldest First"},
|
||||
{"value": "-first_installation_year", "label": "Newest First"},
|
||||
{"value": "total_installations", "label": "Fewest Installations"},
|
||||
{"value": "-total_installations", "label": "Most Installations"},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
|
||||
# === RIDE MODEL STATISTICS ===
|
||||
|
||||
@@ -646,37 +693,23 @@ class RideModelStatsAPIView(APIView):
|
||||
|
||||
# Calculate statistics
|
||||
total_models = RideModel.objects.count()
|
||||
total_installations = (
|
||||
RideModel.objects.aggregate(total=Count("rides"))["total"] or 0
|
||||
)
|
||||
total_installations = RideModel.objects.aggregate(total=Count("rides"))["total"] or 0
|
||||
|
||||
active_manufacturers = (
|
||||
Company.objects.filter(
|
||||
roles__contains=["MANUFACTURER"], ride_models__isnull=False
|
||||
)
|
||||
.distinct()
|
||||
.count()
|
||||
Company.objects.filter(roles__contains=["MANUFACTURER"], ride_models__isnull=False).distinct().count()
|
||||
)
|
||||
|
||||
discontinued_models = RideModel.objects.filter(is_discontinued=True).count()
|
||||
|
||||
# Category breakdown
|
||||
by_category = {}
|
||||
category_counts = (
|
||||
RideModel.objects.exclude(category="")
|
||||
.values("category")
|
||||
.annotate(count=Count("id"))
|
||||
)
|
||||
category_counts = RideModel.objects.exclude(category="").values("category").annotate(count=Count("id"))
|
||||
for item in category_counts:
|
||||
by_category[item["category"]] = item["count"]
|
||||
|
||||
# Target market breakdown
|
||||
by_target_market = {}
|
||||
market_counts = (
|
||||
RideModel.objects.exclude(target_market="")
|
||||
.values("target_market")
|
||||
.annotate(count=Count("id"))
|
||||
)
|
||||
market_counts = RideModel.objects.exclude(target_market="").values("target_market").annotate(count=Count("id"))
|
||||
for item in market_counts:
|
||||
by_target_market[item["target_market"]] = item["count"]
|
||||
|
||||
@@ -693,9 +726,7 @@ class RideModelStatsAPIView(APIView):
|
||||
|
||||
# Recent models (last 30 days)
|
||||
thirty_days_ago = timezone.now() - timedelta(days=30)
|
||||
recent_models = RideModel.objects.filter(
|
||||
created_at__gte=thirty_days_ago
|
||||
).count()
|
||||
recent_models = RideModel.objects.filter(created_at__gte=thirty_days_ago).count()
|
||||
|
||||
return Response(
|
||||
{
|
||||
@@ -730,7 +761,7 @@ class RideModelVariantListCreateAPIView(APIView):
|
||||
try:
|
||||
ride_model = RideModel.objects.get(pk=ride_model_pk)
|
||||
except RideModel.DoesNotExist:
|
||||
raise NotFound("Ride model not found")
|
||||
raise NotFound("Ride model not found") from None
|
||||
|
||||
variants = RideModelVariant.objects.filter(ride_model=ride_model)
|
||||
serializer = RideModelVariantOutputSerializer(variants, many=True)
|
||||
@@ -753,7 +784,7 @@ class RideModelVariantListCreateAPIView(APIView):
|
||||
try:
|
||||
ride_model = RideModel.objects.get(pk=ride_model_pk)
|
||||
except RideModel.DoesNotExist:
|
||||
raise NotFound("Ride model not found")
|
||||
raise NotFound("Ride model not found") from None
|
||||
|
||||
# Override ride_model_id in the data
|
||||
data = request.data.copy()
|
||||
@@ -787,7 +818,7 @@ class RideModelVariantDetailAPIView(APIView):
|
||||
try:
|
||||
return RideModelVariant.objects.get(ride_model_id=ride_model_pk, pk=pk)
|
||||
except RideModelVariant.DoesNotExist:
|
||||
raise NotFound("Variant not found")
|
||||
raise NotFound("Variant not found") from None
|
||||
|
||||
@extend_schema(
|
||||
summary="Get a ride model variant",
|
||||
@@ -807,9 +838,7 @@ class RideModelVariantDetailAPIView(APIView):
|
||||
)
|
||||
def patch(self, request: Request, ride_model_pk: int, pk: int) -> Response:
|
||||
variant = self._get_variant_or_404(ride_model_pk, pk)
|
||||
serializer_in = RideModelVariantUpdateInputSerializer(
|
||||
data=request.data, partial=True
|
||||
)
|
||||
serializer_in = RideModelVariantUpdateInputSerializer(data=request.data, partial=True)
|
||||
serializer_in.is_valid(raise_exception=True)
|
||||
|
||||
for field, value in serializer_in.validated_data.items():
|
||||
|
||||
@@ -118,9 +118,7 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
|
||||
def get_queryset(self): # type: ignore[override]
|
||||
"""Get photos for the current ride with optimized queries."""
|
||||
queryset = RidePhoto.objects.select_related(
|
||||
"ride", "ride__park", "ride__park__operator", "uploaded_by"
|
||||
)
|
||||
queryset = RidePhoto.objects.select_related("ride", "ride__park", "ride__park__operator", "uploaded_by")
|
||||
|
||||
# If ride_pk is provided in URL kwargs, filter by ride
|
||||
ride_pk = self.kwargs.get("ride_pk")
|
||||
@@ -149,7 +147,7 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
try:
|
||||
ride = Ride.objects.get(pk=ride_id)
|
||||
except Ride.DoesNotExist:
|
||||
raise ValidationError("Ride not found")
|
||||
raise ValidationError("Ride not found") from None
|
||||
|
||||
try:
|
||||
# Use the service to create the photo with proper business logic
|
||||
@@ -169,17 +167,14 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating ride photo: {e}")
|
||||
raise ValidationError(f"Failed to create photo: {str(e)}")
|
||||
raise ValidationError(f"Failed to create photo: {str(e)}") from None
|
||||
|
||||
def perform_update(self, serializer):
|
||||
"""Update ride photo with permission checking."""
|
||||
instance = self.get_object()
|
||||
|
||||
# Check permissions - allow owner or staff
|
||||
if not (
|
||||
self.request.user == instance.uploaded_by
|
||||
or getattr(self.request.user, "is_staff", False)
|
||||
):
|
||||
if not (self.request.user == instance.uploaded_by or getattr(self.request.user, "is_staff", False)):
|
||||
raise PermissionDenied("You can only edit your own photos or be an admin.")
|
||||
|
||||
# Handle primary photo logic using service
|
||||
@@ -191,39 +186,31 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
del serializer.validated_data["is_primary"]
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting primary photo: {e}")
|
||||
raise ValidationError(f"Failed to set primary photo: {str(e)}")
|
||||
raise ValidationError(f"Failed to set primary photo: {str(e)}") from None
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""Delete ride photo with permission checking."""
|
||||
# Check permissions - allow owner or staff
|
||||
if not (
|
||||
self.request.user == instance.uploaded_by
|
||||
or getattr(self.request.user, "is_staff", False)
|
||||
):
|
||||
raise PermissionDenied(
|
||||
"You can only delete your own photos or be an admin."
|
||||
)
|
||||
if not (self.request.user == instance.uploaded_by or getattr(self.request.user, "is_staff", False)):
|
||||
raise PermissionDenied("You can only delete your own photos or be an admin.")
|
||||
|
||||
try:
|
||||
# Delete from Cloudflare first if image exists
|
||||
if instance.image:
|
||||
try:
|
||||
from django_cloudflareimages_toolkit.services import CloudflareImagesService
|
||||
|
||||
service = CloudflareImagesService()
|
||||
service.delete_image(instance.image)
|
||||
logger.info(
|
||||
f"Successfully deleted ride photo from Cloudflare: {instance.image.cloudflare_id}")
|
||||
logger.info(f"Successfully deleted ride photo from Cloudflare: {instance.image.cloudflare_id}")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to delete ride photo from Cloudflare: {str(e)}")
|
||||
logger.error(f"Failed to delete ride photo from Cloudflare: {str(e)}")
|
||||
# Continue with database deletion even if Cloudflare deletion fails
|
||||
|
||||
RideMediaService.delete_photo(
|
||||
instance, deleted_by=self.request.user # type: ignore
|
||||
)
|
||||
RideMediaService.delete_photo(instance, deleted_by=self.request.user) # type: ignore
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting ride photo: {e}")
|
||||
raise ValidationError(f"Failed to delete photo: {str(e)}")
|
||||
raise ValidationError(f"Failed to delete photo: {str(e)}") from None
|
||||
|
||||
@extend_schema(
|
||||
summary="Set photo as primary",
|
||||
@@ -242,13 +229,8 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
photo = self.get_object()
|
||||
|
||||
# Check permissions - allow owner or staff
|
||||
if not (
|
||||
request.user == photo.uploaded_by
|
||||
or getattr(request.user, "is_staff", False)
|
||||
):
|
||||
raise PermissionDenied(
|
||||
"You can only modify your own photos or be an admin."
|
||||
)
|
||||
if not (request.user == photo.uploaded_by or getattr(request.user, "is_staff", False)):
|
||||
raise PermissionDenied("You can only modify your own photos or be an admin.")
|
||||
|
||||
try:
|
||||
success = RideMediaService.set_primary_photo(ride=photo.ride, photo=photo)
|
||||
@@ -260,21 +242,21 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
|
||||
return Response(
|
||||
{
|
||||
"message": "Photo set as primary successfully",
|
||||
"detail": "Photo set as primary successfully",
|
||||
"photo": serializer.data,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "Failed to set primary photo"},
|
||||
{"detail": "Failed to set primary photo"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting primary photo: {e}")
|
||||
return Response(
|
||||
{"error": f"Failed to set primary photo: {str(e)}"},
|
||||
{"detail": f"Failed to set primary photo: {str(e)}"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -305,7 +287,7 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
|
||||
if photo_ids is None or approve is None:
|
||||
return Response(
|
||||
{"error": "Missing required fields: photo_ids and/or approve."},
|
||||
{"detail": "Missing required fields: photo_ids and/or approve."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -319,7 +301,7 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
|
||||
return Response(
|
||||
{
|
||||
"message": f"Successfully {'approved' if approve else 'rejected'} {updated_count} photos",
|
||||
"detail": f"Successfully {'approved' if approve else 'rejected'} {updated_count} photos",
|
||||
"updated_count": updated_count,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
@@ -328,7 +310,7 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
except Exception as e:
|
||||
logger.error(f"Error in bulk photo approval: {e}")
|
||||
return Response(
|
||||
{"error": f"Failed to update photos: {str(e)}"},
|
||||
{"detail": f"Failed to update photos: {str(e)}"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -352,7 +334,7 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
ride = Ride.objects.get(pk=ride_pk)
|
||||
except Ride.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Ride not found."},
|
||||
{"detail": "Ride not found."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
@@ -363,16 +345,10 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
# Global stats across all rides
|
||||
stats = {
|
||||
"total_photos": RidePhoto.objects.count(),
|
||||
"approved_photos": RidePhoto.objects.filter(
|
||||
is_approved=True
|
||||
).count(),
|
||||
"pending_photos": RidePhoto.objects.filter(
|
||||
is_approved=False
|
||||
).count(),
|
||||
"approved_photos": RidePhoto.objects.filter(is_approved=True).count(),
|
||||
"pending_photos": RidePhoto.objects.filter(is_approved=False).count(),
|
||||
"has_primary": False, # Not applicable for global stats
|
||||
"recent_uploads": RidePhoto.objects.order_by("-created_at")[
|
||||
:5
|
||||
].count(),
|
||||
"recent_uploads": RidePhoto.objects.order_by("-created_at")[:5].count(),
|
||||
"by_type": {},
|
||||
}
|
||||
|
||||
@@ -382,7 +358,7 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting ride photo stats: {e}")
|
||||
return Response(
|
||||
{"error": f"Failed to get photo statistics: {str(e)}"},
|
||||
{"detail": f"Failed to get photo statistics: {str(e)}"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
@@ -401,26 +377,23 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
def set_primary_legacy(self, request, id=None):
|
||||
"""Legacy set primary action for backwards compatibility."""
|
||||
photo = self.get_object()
|
||||
if not (
|
||||
request.user == photo.uploaded_by
|
||||
or request.user.has_perm("rides.change_ridephoto")
|
||||
):
|
||||
if not (request.user == photo.uploaded_by or request.user.has_perm("rides.change_ridephoto")):
|
||||
return Response(
|
||||
{"error": "You do not have permission to edit photos for this ride."},
|
||||
{"detail": "You do not have permission to edit photos for this ride."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
try:
|
||||
success = RideMediaService.set_primary_photo(ride=photo.ride, photo=photo)
|
||||
if success:
|
||||
return Response({"message": "Photo set as primary successfully."})
|
||||
return Response({"detail": "Photo set as primary successfully."})
|
||||
else:
|
||||
return Response(
|
||||
{"error": "Failed to set primary photo"},
|
||||
{"detail": "Failed to set primary photo"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in set_primary_photo: {str(e)}", exc_info=True)
|
||||
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@extend_schema(
|
||||
summary="Save Cloudflare image as ride photo",
|
||||
@@ -440,7 +413,7 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
ride_pk = self.kwargs.get("ride_pk")
|
||||
if not ride_pk:
|
||||
return Response(
|
||||
{"error": "Ride ID is required"},
|
||||
{"detail": "Ride ID is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -448,14 +421,14 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
ride = Ride.objects.get(pk=ride_pk)
|
||||
except Ride.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Ride not found"},
|
||||
{"detail": "Ride not found"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
cloudflare_image_id = request.data.get("cloudflare_image_id")
|
||||
if not cloudflare_image_id:
|
||||
return Response(
|
||||
{"error": "cloudflare_image_id is required"},
|
||||
{"detail": "cloudflare_image_id is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -473,27 +446,25 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
|
||||
if not image_data:
|
||||
return Response(
|
||||
{"error": "Image not found in Cloudflare"},
|
||||
{"detail": "Image not found in Cloudflare"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Try to find existing CloudflareImage record by cloudflare_id
|
||||
cloudflare_image = None
|
||||
try:
|
||||
cloudflare_image = CloudflareImage.objects.get(
|
||||
cloudflare_id=cloudflare_image_id)
|
||||
cloudflare_image = CloudflareImage.objects.get(cloudflare_id=cloudflare_image_id)
|
||||
|
||||
# Update existing record with latest data from Cloudflare
|
||||
cloudflare_image.status = 'uploaded'
|
||||
cloudflare_image.status = "uploaded"
|
||||
cloudflare_image.uploaded_at = timezone.now()
|
||||
cloudflare_image.metadata = image_data.get('meta', {})
|
||||
cloudflare_image.metadata = image_data.get("meta", {})
|
||||
# Extract variants from nested result structure
|
||||
cloudflare_image.variants = image_data.get(
|
||||
'result', {}).get('variants', [])
|
||||
cloudflare_image.variants = image_data.get("result", {}).get("variants", [])
|
||||
cloudflare_image.cloudflare_metadata = image_data
|
||||
cloudflare_image.width = image_data.get('width')
|
||||
cloudflare_image.height = image_data.get('height')
|
||||
cloudflare_image.format = image_data.get('format', '')
|
||||
cloudflare_image.width = image_data.get("width")
|
||||
cloudflare_image.height = image_data.get("height")
|
||||
cloudflare_image.format = image_data.get("format", "")
|
||||
cloudflare_image.save()
|
||||
|
||||
except CloudflareImage.DoesNotExist:
|
||||
@@ -501,24 +472,23 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
cloudflare_image = CloudflareImage.objects.create(
|
||||
cloudflare_id=cloudflare_image_id,
|
||||
user=request.user,
|
||||
status='uploaded',
|
||||
upload_url='', # Not needed for uploaded images
|
||||
status="uploaded",
|
||||
upload_url="", # Not needed for uploaded images
|
||||
expires_at=timezone.now() + timezone.timedelta(days=365), # Set far future expiry
|
||||
uploaded_at=timezone.now(),
|
||||
metadata=image_data.get('meta', {}),
|
||||
metadata=image_data.get("meta", {}),
|
||||
# Extract variants from nested result structure
|
||||
variants=image_data.get('result', {}).get('variants', []),
|
||||
variants=image_data.get("result", {}).get("variants", []),
|
||||
cloudflare_metadata=image_data,
|
||||
width=image_data.get('width'),
|
||||
height=image_data.get('height'),
|
||||
format=image_data.get('format', ''),
|
||||
width=image_data.get("width"),
|
||||
height=image_data.get("height"),
|
||||
format=image_data.get("format", ""),
|
||||
)
|
||||
|
||||
except Exception as api_error:
|
||||
logger.error(
|
||||
f"Error fetching image from Cloudflare API: {str(api_error)}", exc_info=True)
|
||||
logger.error(f"Error fetching image from Cloudflare API: {str(api_error)}", exc_info=True)
|
||||
return Response(
|
||||
{"error": f"Failed to fetch image from Cloudflare: {str(api_error)}"},
|
||||
{"detail": f"Failed to fetch image from Cloudflare: {str(api_error)}"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -548,6 +518,6 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving ride photo: {e}")
|
||||
return Response(
|
||||
{"error": f"Failed to save photo: {str(e)}"},
|
||||
{"detail": f"Failed to save photo: {str(e)}"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -52,18 +52,14 @@ from apps.rides.models import Ride, RidePhoto
|
||||
class RidePhotoOutputSerializer(serializers.ModelSerializer):
|
||||
"""Output serializer for ride photos with Cloudflare Images support."""
|
||||
|
||||
uploaded_by_username = serializers.CharField(
|
||||
source="uploaded_by.username", read_only=True
|
||||
)
|
||||
uploaded_by_username = serializers.CharField(source="uploaded_by.username", read_only=True)
|
||||
|
||||
file_size = serializers.SerializerMethodField()
|
||||
dimensions = serializers.SerializerMethodField()
|
||||
image_url = serializers.SerializerMethodField()
|
||||
image_variants = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(
|
||||
serializers.IntegerField(allow_null=True, help_text="File size in bytes")
|
||||
)
|
||||
@extend_schema_field(serializers.IntegerField(allow_null=True, help_text="File size in bytes"))
|
||||
def get_file_size(self, obj):
|
||||
"""Get file size in bytes."""
|
||||
return obj.file_size
|
||||
@@ -81,11 +77,7 @@ class RidePhotoOutputSerializer(serializers.ModelSerializer):
|
||||
"""Get image dimensions as [width, height]."""
|
||||
return obj.dimensions
|
||||
|
||||
@extend_schema_field(
|
||||
serializers.URLField(
|
||||
help_text="Full URL to the Cloudflare Images asset", allow_null=True
|
||||
)
|
||||
)
|
||||
@extend_schema_field(serializers.URLField(help_text="Full URL to the Cloudflare Images asset", allow_null=True))
|
||||
def get_image_url(self, obj):
|
||||
"""Get the full Cloudflare Images URL."""
|
||||
if obj.image:
|
||||
@@ -186,9 +178,7 @@ class RidePhotoUpdateInputSerializer(serializers.ModelSerializer):
|
||||
class RidePhotoListOutputSerializer(serializers.ModelSerializer):
|
||||
"""Simplified output serializer for ride photo lists."""
|
||||
|
||||
uploaded_by_username = serializers.CharField(
|
||||
source="uploaded_by.username", read_only=True
|
||||
)
|
||||
uploaded_by_username = serializers.CharField(source="uploaded_by.username", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = RidePhoto
|
||||
@@ -208,12 +198,8 @@ class RidePhotoListOutputSerializer(serializers.ModelSerializer):
|
||||
class RidePhotoApprovalInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for photo approval operations."""
|
||||
|
||||
photo_ids = serializers.ListField(
|
||||
child=serializers.IntegerField(), help_text="List of photo IDs to approve"
|
||||
)
|
||||
approve = serializers.BooleanField(
|
||||
default=True, help_text="Whether to approve (True) or reject (False) the photos"
|
||||
)
|
||||
photo_ids = serializers.ListField(child=serializers.IntegerField(), help_text="List of photo IDs to approve")
|
||||
approve = serializers.BooleanField(default=True, help_text="Whether to approve (True) or reject (False) the photos")
|
||||
|
||||
|
||||
class RidePhotoStatsOutputSerializer(serializers.Serializer):
|
||||
@@ -224,9 +210,7 @@ class RidePhotoStatsOutputSerializer(serializers.Serializer):
|
||||
pending_photos = serializers.IntegerField()
|
||||
has_primary = serializers.BooleanField()
|
||||
recent_uploads = serializers.IntegerField()
|
||||
by_type = serializers.DictField(
|
||||
child=serializers.IntegerField(), help_text="Photo counts by type"
|
||||
)
|
||||
by_type = serializers.DictField(child=serializers.IntegerField(), help_text="Photo counts by type")
|
||||
|
||||
|
||||
class RidePhotoTypeFilterSerializer(serializers.Serializer):
|
||||
@@ -292,8 +276,12 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
ride_model_name = serializers.CharField(source="ride_model.name", read_only=True, allow_null=True)
|
||||
ride_model_slug = serializers.CharField(source="ride_model.slug", read_only=True, allow_null=True)
|
||||
ride_model_category = serializers.CharField(source="ride_model.category", read_only=True, allow_null=True)
|
||||
ride_model_manufacturer_name = serializers.CharField(source="ride_model.manufacturer.name", read_only=True, allow_null=True)
|
||||
ride_model_manufacturer_slug = serializers.CharField(source="ride_model.manufacturer.slug", read_only=True, allow_null=True)
|
||||
ride_model_manufacturer_name = serializers.CharField(
|
||||
source="ride_model.manufacturer.name", read_only=True, allow_null=True
|
||||
)
|
||||
ride_model_manufacturer_slug = serializers.CharField(
|
||||
source="ride_model.manufacturer.slug", read_only=True, allow_null=True
|
||||
)
|
||||
|
||||
# Roller coaster stats fields
|
||||
coaster_height_ft = serializers.SerializerMethodField()
|
||||
@@ -323,7 +311,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_park_city(self, obj):
|
||||
"""Get city from park location."""
|
||||
try:
|
||||
if obj.park and hasattr(obj.park, 'location') and obj.park.location:
|
||||
if obj.park and hasattr(obj.park, "location") and obj.park.location:
|
||||
return obj.park.location.city
|
||||
return None
|
||||
except AttributeError:
|
||||
@@ -333,7 +321,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_park_state(self, obj):
|
||||
"""Get state from park location."""
|
||||
try:
|
||||
if obj.park and hasattr(obj.park, 'location') and obj.park.location:
|
||||
if obj.park and hasattr(obj.park, "location") and obj.park.location:
|
||||
return obj.park.location.state
|
||||
return None
|
||||
except AttributeError:
|
||||
@@ -343,7 +331,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_park_country(self, obj):
|
||||
"""Get country from park location."""
|
||||
try:
|
||||
if obj.park and hasattr(obj.park, 'location') and obj.park.location:
|
||||
if obj.park and hasattr(obj.park, "location") and obj.park.location:
|
||||
return obj.park.location.country
|
||||
return None
|
||||
except AttributeError:
|
||||
@@ -353,7 +341,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_coaster_height_ft(self, obj):
|
||||
"""Get roller coaster height."""
|
||||
try:
|
||||
if hasattr(obj, 'coaster_stats') and obj.coaster_stats:
|
||||
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
||||
return float(obj.coaster_stats.height_ft) if obj.coaster_stats.height_ft else None
|
||||
return None
|
||||
except (AttributeError, TypeError):
|
||||
@@ -363,7 +351,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_coaster_length_ft(self, obj):
|
||||
"""Get roller coaster length."""
|
||||
try:
|
||||
if hasattr(obj, 'coaster_stats') and obj.coaster_stats:
|
||||
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
||||
return float(obj.coaster_stats.length_ft) if obj.coaster_stats.length_ft else None
|
||||
return None
|
||||
except (AttributeError, TypeError):
|
||||
@@ -373,7 +361,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_coaster_speed_mph(self, obj):
|
||||
"""Get roller coaster speed."""
|
||||
try:
|
||||
if hasattr(obj, 'coaster_stats') and obj.coaster_stats:
|
||||
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
||||
return float(obj.coaster_stats.speed_mph) if obj.coaster_stats.speed_mph else None
|
||||
return None
|
||||
except (AttributeError, TypeError):
|
||||
@@ -383,7 +371,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_coaster_inversions(self, obj):
|
||||
"""Get roller coaster inversions."""
|
||||
try:
|
||||
if hasattr(obj, 'coaster_stats') and obj.coaster_stats:
|
||||
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
||||
return obj.coaster_stats.inversions
|
||||
return None
|
||||
except AttributeError:
|
||||
@@ -393,7 +381,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_coaster_ride_time_seconds(self, obj):
|
||||
"""Get roller coaster ride time."""
|
||||
try:
|
||||
if hasattr(obj, 'coaster_stats') and obj.coaster_stats:
|
||||
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
||||
return obj.coaster_stats.ride_time_seconds
|
||||
return None
|
||||
except AttributeError:
|
||||
@@ -403,7 +391,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_coaster_track_type(self, obj):
|
||||
"""Get roller coaster track type."""
|
||||
try:
|
||||
if hasattr(obj, 'coaster_stats') and obj.coaster_stats:
|
||||
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
||||
return obj.coaster_stats.track_type
|
||||
return None
|
||||
except AttributeError:
|
||||
@@ -413,7 +401,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_coaster_track_material(self, obj):
|
||||
"""Get roller coaster track material."""
|
||||
try:
|
||||
if hasattr(obj, 'coaster_stats') and obj.coaster_stats:
|
||||
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
||||
return obj.coaster_stats.track_material
|
||||
return None
|
||||
except AttributeError:
|
||||
@@ -423,7 +411,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_coaster_roller_coaster_type(self, obj):
|
||||
"""Get roller coaster type."""
|
||||
try:
|
||||
if hasattr(obj, 'coaster_stats') and obj.coaster_stats:
|
||||
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
||||
return obj.coaster_stats.roller_coaster_type
|
||||
return None
|
||||
except AttributeError:
|
||||
@@ -433,7 +421,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_coaster_max_drop_height_ft(self, obj):
|
||||
"""Get roller coaster max drop height."""
|
||||
try:
|
||||
if hasattr(obj, 'coaster_stats') and obj.coaster_stats:
|
||||
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
||||
return float(obj.coaster_stats.max_drop_height_ft) if obj.coaster_stats.max_drop_height_ft else None
|
||||
return None
|
||||
except (AttributeError, TypeError):
|
||||
@@ -443,7 +431,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_coaster_propulsion_system(self, obj):
|
||||
"""Get roller coaster propulsion system."""
|
||||
try:
|
||||
if hasattr(obj, 'coaster_stats') and obj.coaster_stats:
|
||||
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
||||
return obj.coaster_stats.propulsion_system
|
||||
return None
|
||||
except AttributeError:
|
||||
@@ -453,7 +441,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_coaster_train_style(self, obj):
|
||||
"""Get roller coaster train style."""
|
||||
try:
|
||||
if hasattr(obj, 'coaster_stats') and obj.coaster_stats:
|
||||
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
||||
return obj.coaster_stats.train_style
|
||||
return None
|
||||
except AttributeError:
|
||||
@@ -463,7 +451,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_coaster_trains_count(self, obj):
|
||||
"""Get roller coaster trains count."""
|
||||
try:
|
||||
if hasattr(obj, 'coaster_stats') and obj.coaster_stats:
|
||||
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
||||
return obj.coaster_stats.trains_count
|
||||
return None
|
||||
except AttributeError:
|
||||
@@ -473,7 +461,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_coaster_cars_per_train(self, obj):
|
||||
"""Get roller coaster cars per train."""
|
||||
try:
|
||||
if hasattr(obj, 'coaster_stats') and obj.coaster_stats:
|
||||
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
||||
return obj.coaster_stats.cars_per_train
|
||||
return None
|
||||
except AttributeError:
|
||||
@@ -483,7 +471,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
def get_coaster_seats_per_car(self, obj):
|
||||
"""Get roller coaster seats per car."""
|
||||
try:
|
||||
if hasattr(obj, 'coaster_stats') and obj.coaster_stats:
|
||||
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
||||
return obj.coaster_stats.seats_per_car
|
||||
return None
|
||||
except AttributeError:
|
||||
@@ -514,44 +502,37 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
"category",
|
||||
"status",
|
||||
"post_closing_status",
|
||||
|
||||
# Dates and computed fields
|
||||
"opening_date",
|
||||
"closing_date",
|
||||
"status_since",
|
||||
"opening_year",
|
||||
|
||||
# Park fields
|
||||
"park_name",
|
||||
"park_slug",
|
||||
"park_city",
|
||||
"park_state",
|
||||
"park_country",
|
||||
|
||||
# Park area fields
|
||||
"park_area_name",
|
||||
"park_area_slug",
|
||||
|
||||
# Company fields
|
||||
"manufacturer_name",
|
||||
"manufacturer_slug",
|
||||
"designer_name",
|
||||
"designer_slug",
|
||||
|
||||
# Ride model fields
|
||||
"ride_model_name",
|
||||
"ride_model_slug",
|
||||
"ride_model_category",
|
||||
"ride_model_manufacturer_name",
|
||||
"ride_model_manufacturer_slug",
|
||||
|
||||
# Ride specifications
|
||||
"min_height_in",
|
||||
"max_height_in",
|
||||
"capacity_per_hour",
|
||||
"ride_duration_seconds",
|
||||
"average_rating",
|
||||
|
||||
# Roller coaster stats
|
||||
"coaster_height_ft",
|
||||
"coaster_length_ft",
|
||||
@@ -567,18 +548,14 @@ class HybridRideSerializer(serializers.ModelSerializer):
|
||||
"coaster_trains_count",
|
||||
"coaster_cars_per_train",
|
||||
"coaster_seats_per_car",
|
||||
|
||||
# Images
|
||||
"banner_image_url",
|
||||
"card_image_url",
|
||||
|
||||
# URLs
|
||||
"url",
|
||||
"park_url",
|
||||
|
||||
# Computed fields for filtering
|
||||
"search_text",
|
||||
|
||||
# Metadata
|
||||
"created_at",
|
||||
"updated_at",
|
||||
|
||||
@@ -35,11 +35,9 @@ app_name = "api_v1_rides"
|
||||
urlpatterns = [
|
||||
# Core list/create endpoints
|
||||
path("", RideListCreateAPIView.as_view(), name="ride-list-create"),
|
||||
|
||||
# Hybrid filtering endpoints
|
||||
path("hybrid/", HybridRideAPIView.as_view(), name="ride-hybrid-filtering"),
|
||||
path("hybrid/filter-metadata/", RideFilterMetadataAPIView.as_view(), name="ride-hybrid-filter-metadata"),
|
||||
|
||||
# Filter options
|
||||
path("filter-options/", FilterOptionsAPIView.as_view(), name="ride-filter-options"),
|
||||
# Autocomplete / suggestion endpoints
|
||||
@@ -61,7 +59,6 @@ urlpatterns = [
|
||||
# Manufacturer and Designer endpoints
|
||||
path("manufacturers/", ManufacturerListAPIView.as_view(), name="manufacturer-list"),
|
||||
path("designers/", DesignerListAPIView.as_view(), name="designer-list"),
|
||||
|
||||
# Ride model management endpoints - nested under rides/manufacturers
|
||||
path(
|
||||
"manufacturers/<slug:manufacturer_slug>/",
|
||||
|
||||
@@ -28,6 +28,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Count
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
|
||||
from rest_framework import permissions, status
|
||||
@@ -333,9 +334,7 @@ class RideListCreateAPIView(APIView):
|
||||
|
||||
paginator = StandardResultsSetPagination()
|
||||
page = paginator.paginate_queryset(qs, request)
|
||||
serializer = RideListOutputSerializer(
|
||||
page, many=True, context={"request": request}
|
||||
)
|
||||
serializer = RideListOutputSerializer(page, many=True, context={"request": request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
def _apply_filters(self, qs, params):
|
||||
@@ -567,9 +566,9 @@ class RideListCreateAPIView(APIView):
|
||||
if ordering in valid_orderings:
|
||||
if ordering in ["height_ft", "-height_ft", "speed_mph", "-speed_mph"]:
|
||||
# For coaster stats ordering, we need to join and order by the stats
|
||||
ordering_field = ordering.replace(
|
||||
"height_ft", "coaster_stats__height_ft"
|
||||
).replace("speed_mph", "coaster_stats__speed_mph")
|
||||
ordering_field = ordering.replace("height_ft", "coaster_stats__height_ft").replace(
|
||||
"speed_mph", "coaster_stats__speed_mph"
|
||||
)
|
||||
qs = qs.order_by(ordering_field)
|
||||
else:
|
||||
qs = qs.order_by(ordering)
|
||||
@@ -602,7 +601,7 @@ class RideListCreateAPIView(APIView):
|
||||
try:
|
||||
park = Park.objects.get(id=validated["park_id"]) # type: ignore
|
||||
except Park.DoesNotExist: # type: ignore
|
||||
raise NotFound("Park not found")
|
||||
raise NotFound("Park not found") from None
|
||||
|
||||
ride = Ride.objects.create( # type: ignore
|
||||
name=validated["name"],
|
||||
@@ -658,7 +657,7 @@ class RideDetailAPIView(APIView):
|
||||
try:
|
||||
return Ride.objects.select_related("park").get(pk=pk) # type: ignore
|
||||
except Ride.DoesNotExist: # type: ignore
|
||||
raise NotFound("Ride not found")
|
||||
raise NotFound("Ride not found") from None
|
||||
|
||||
@cache_api_response(timeout=1800, key_prefix="ride_detail")
|
||||
def get(self, request: Request, pk: int) -> Response:
|
||||
@@ -672,9 +671,7 @@ class RideDetailAPIView(APIView):
|
||||
serializer_in.is_valid(raise_exception=True)
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{
|
||||
"detail": "Ride update is not available because domain models are not imported."
|
||||
},
|
||||
{"detail": "Ride update is not available because domain models are not imported."},
|
||||
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
)
|
||||
|
||||
@@ -690,7 +687,7 @@ class RideDetailAPIView(APIView):
|
||||
# Use the move_to_park method for proper handling
|
||||
park_change_info = ride.move_to_park(new_park)
|
||||
except Park.DoesNotExist: # type: ignore
|
||||
raise NotFound("Target park not found")
|
||||
raise NotFound("Target park not found") from None
|
||||
|
||||
# Apply other field updates
|
||||
for key, value in validated_data.items():
|
||||
@@ -715,9 +712,7 @@ class RideDetailAPIView(APIView):
|
||||
def delete(self, request: Request, pk: int) -> Response:
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{
|
||||
"detail": "Ride delete is not available because domain models are not imported."
|
||||
},
|
||||
{"detail": "Ride delete is not available because domain models are not imported."},
|
||||
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
)
|
||||
ride = self._get_ride_or_404(pk)
|
||||
@@ -1491,16 +1486,12 @@ class FilterOptionsAPIView(APIView):
|
||||
|
||||
# Get manufacturers (companies with MANUFACTURER role)
|
||||
manufacturers = list(
|
||||
Company.objects.filter(roles__contains=["MANUFACTURER"])
|
||||
.values("id", "name", "slug")
|
||||
.order_by("name")
|
||||
Company.objects.filter(roles__contains=["MANUFACTURER"]).values("id", "name", "slug").order_by("name")
|
||||
)
|
||||
|
||||
# Get designers (companies with DESIGNER role)
|
||||
designers = list(
|
||||
Company.objects.filter(roles__contains=["DESIGNER"])
|
||||
.values("id", "name", "slug")
|
||||
.order_by("name")
|
||||
Company.objects.filter(roles__contains=["DESIGNER"]).values("id", "name", "slug").order_by("name")
|
||||
)
|
||||
|
||||
# Get ride models data from database
|
||||
@@ -1722,11 +1713,7 @@ class FilterOptionsAPIView(APIView):
|
||||
# --- Company search (autocomplete) -----------------------------------------
|
||||
@extend_schema(
|
||||
summary="Search companies (manufacturers/designers) for autocomplete",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
||||
)
|
||||
],
|
||||
parameters=[OpenApiParameter(name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR)],
|
||||
responses={200: OpenApiTypes.OBJECT},
|
||||
tags=["Rides"],
|
||||
)
|
||||
@@ -1753,20 +1740,14 @@ class CompanySearchAPIView(APIView):
|
||||
)
|
||||
|
||||
qs = Company.objects.filter(name__icontains=q)[:20] # type: ignore
|
||||
results = [
|
||||
{"id": c.id, "name": c.name, "slug": getattr(c, "slug", "")} for c in qs
|
||||
]
|
||||
results = [{"id": c.id, "name": c.name, "slug": getattr(c, "slug", "")} for c in qs]
|
||||
return Response(results)
|
||||
|
||||
|
||||
# --- Ride model search (autocomplete) --------------------------------------
|
||||
@extend_schema(
|
||||
summary="Search ride models for autocomplete",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
||||
)
|
||||
],
|
||||
parameters=[OpenApiParameter(name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR)],
|
||||
tags=["Rides"],
|
||||
)
|
||||
class RideModelSearchAPIView(APIView):
|
||||
@@ -1795,21 +1776,14 @@ class RideModelSearchAPIView(APIView):
|
||||
)
|
||||
|
||||
qs = RideModel.objects.filter(name__icontains=q)[:20] # type: ignore
|
||||
results = [
|
||||
{"id": m.id, "name": m.name, "category": getattr(m, "category", "")}
|
||||
for m in qs
|
||||
]
|
||||
results = [{"id": m.id, "name": m.name, "category": getattr(m, "category", "")} for m in qs]
|
||||
return Response(results)
|
||||
|
||||
|
||||
# --- Search suggestions -----------------------------------------------------
|
||||
@extend_schema(
|
||||
summary="Search suggestions for ride search box",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
||||
)
|
||||
],
|
||||
parameters=[OpenApiParameter(name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR)],
|
||||
tags=["Rides"],
|
||||
)
|
||||
class RideSearchSuggestionsAPIView(APIView):
|
||||
@@ -1827,9 +1801,7 @@ class RideSearchSuggestionsAPIView(APIView):
|
||||
|
||||
# Very small suggestion implementation: look in ride names if available
|
||||
if MODELS_AVAILABLE and Ride is not None:
|
||||
qs = Ride.objects.filter(name__icontains=q).values_list("name", flat=True)[
|
||||
:10
|
||||
] # type: ignore
|
||||
qs = Ride.objects.filter(name__icontains=q).values_list("name", flat=True)[:10] # type: ignore
|
||||
return Response([{"suggestion": name} for name in qs])
|
||||
|
||||
# Fallback suggestions
|
||||
@@ -1862,7 +1834,7 @@ class RideImageSettingsAPIView(APIView):
|
||||
try:
|
||||
return Ride.objects.get(pk=pk) # type: ignore
|
||||
except Ride.DoesNotExist: # type: ignore
|
||||
raise NotFound("Ride not found")
|
||||
raise NotFound("Ride not found") from None
|
||||
|
||||
def patch(self, request: Request, pk: int) -> Response:
|
||||
"""Set banner and card images for the ride."""
|
||||
@@ -1878,9 +1850,7 @@ class RideImageSettingsAPIView(APIView):
|
||||
ride.save()
|
||||
|
||||
# Return updated ride data
|
||||
output_serializer = RideDetailOutputSerializer(
|
||||
ride, context={"request": request}
|
||||
)
|
||||
output_serializer = RideDetailOutputSerializer(ride, context={"request": request})
|
||||
return Response(output_serializer.data)
|
||||
|
||||
|
||||
@@ -1902,12 +1872,8 @@ class RideImageSettingsAPIView(APIView):
|
||||
OpenApiTypes.STR,
|
||||
description="Filter by ride status (comma-separated for multiple)",
|
||||
),
|
||||
OpenApiParameter(
|
||||
"park_slug", OpenApiTypes.STR, description="Filter by park slug"
|
||||
),
|
||||
OpenApiParameter(
|
||||
"park_id", OpenApiTypes.INT, description="Filter by park ID"
|
||||
),
|
||||
OpenApiParameter("park_slug", OpenApiTypes.STR, description="Filter by park slug"),
|
||||
OpenApiParameter("park_id", OpenApiTypes.INT, description="Filter by park ID"),
|
||||
OpenApiParameter(
|
||||
"manufacturer",
|
||||
OpenApiTypes.STR,
|
||||
@@ -1923,18 +1889,10 @@ class RideImageSettingsAPIView(APIView):
|
||||
OpenApiTypes.STR,
|
||||
description="Filter by ride model slug (comma-separated for multiple)",
|
||||
),
|
||||
OpenApiParameter(
|
||||
"opening_year_min", OpenApiTypes.INT, description="Minimum opening year"
|
||||
),
|
||||
OpenApiParameter(
|
||||
"opening_year_max", OpenApiTypes.INT, description="Maximum opening year"
|
||||
),
|
||||
OpenApiParameter(
|
||||
"rating_min", OpenApiTypes.NUMBER, description="Minimum average rating"
|
||||
),
|
||||
OpenApiParameter(
|
||||
"rating_max", OpenApiTypes.NUMBER, description="Maximum average rating"
|
||||
),
|
||||
OpenApiParameter("opening_year_min", OpenApiTypes.INT, description="Minimum opening year"),
|
||||
OpenApiParameter("opening_year_max", OpenApiTypes.INT, description="Maximum opening year"),
|
||||
OpenApiParameter("rating_min", OpenApiTypes.NUMBER, description="Minimum average rating"),
|
||||
OpenApiParameter("rating_max", OpenApiTypes.NUMBER, description="Maximum average rating"),
|
||||
OpenApiParameter(
|
||||
"height_requirement_min",
|
||||
OpenApiTypes.INT,
|
||||
@@ -1945,12 +1903,8 @@ class RideImageSettingsAPIView(APIView):
|
||||
OpenApiTypes.INT,
|
||||
description="Maximum height requirement in inches",
|
||||
),
|
||||
OpenApiParameter(
|
||||
"capacity_min", OpenApiTypes.INT, description="Minimum hourly capacity"
|
||||
),
|
||||
OpenApiParameter(
|
||||
"capacity_max", OpenApiTypes.INT, description="Maximum hourly capacity"
|
||||
),
|
||||
OpenApiParameter("capacity_min", OpenApiTypes.INT, description="Minimum hourly capacity"),
|
||||
OpenApiParameter("capacity_max", OpenApiTypes.INT, description="Maximum hourly capacity"),
|
||||
OpenApiParameter(
|
||||
"roller_coaster_type",
|
||||
OpenApiTypes.STR,
|
||||
@@ -2022,9 +1976,7 @@ class RideImageSettingsAPIView(APIView):
|
||||
"properties": {
|
||||
"rides": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/HybridRideSerializer"
|
||||
},
|
||||
"items": {"$ref": "#/components/schemas/HybridRideSerializer"},
|
||||
},
|
||||
"total_count": {"type": "integer"},
|
||||
"strategy": {
|
||||
@@ -2084,7 +2036,7 @@ class HybridRideAPIView(APIView):
|
||||
data = smart_ride_loader.get_progressive_load(offset, filters)
|
||||
except ValueError:
|
||||
return Response(
|
||||
{"error": "Invalid offset parameter"},
|
||||
{"detail": "Invalid offset parameter"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
@@ -2109,7 +2061,7 @@ class HybridRideAPIView(APIView):
|
||||
except Exception as e:
|
||||
logger.error(f"Error in HybridRideAPIView: {e}")
|
||||
return Response(
|
||||
{"error": "Internal server error"},
|
||||
{"detail": "Internal server error"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
@@ -2158,7 +2110,7 @@ class HybridRideAPIView(APIView):
|
||||
for param in int_params:
|
||||
value = query_params.get(param)
|
||||
if value:
|
||||
try:
|
||||
try: # noqa: SIM105
|
||||
filters[param] = int(value)
|
||||
except ValueError:
|
||||
pass # Skip invalid integer values
|
||||
@@ -2175,7 +2127,7 @@ class HybridRideAPIView(APIView):
|
||||
for param in float_params:
|
||||
value = query_params.get(param)
|
||||
if value:
|
||||
try:
|
||||
try: # noqa: SIM105
|
||||
filters[param] = float(value)
|
||||
except ValueError:
|
||||
pass # Skip invalid float values
|
||||
@@ -2408,7 +2360,7 @@ class RideFilterMetadataAPIView(APIView):
|
||||
except Exception as e:
|
||||
logger.error(f"Error in RideFilterMetadataAPIView: {e}")
|
||||
return Response(
|
||||
{"error": "Internal server error"},
|
||||
{"detail": "Internal server error"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
@@ -2417,18 +2369,18 @@ class RideFilterMetadataAPIView(APIView):
|
||||
# Reuse the same filter extraction logic
|
||||
view = HybridRideAPIView()
|
||||
return view._extract_filters(query_params)
|
||||
|
||||
|
||||
# === MANUFACTURER & DESIGNER LISTS ===
|
||||
|
||||
|
||||
class BaseCompanyListAPIView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
role = None
|
||||
|
||||
def get(self, request: Request) -> Response:
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{"detail": "Models not available"},
|
||||
status=status.HTTP_501_NOT_IMPLEMENTED
|
||||
)
|
||||
return Response({"detail": "Models not available"}, status=status.HTTP_501_NOT_IMPLEMENTED)
|
||||
|
||||
companies = (
|
||||
Company.objects.filter(roles__contains=[self.role])
|
||||
@@ -2448,10 +2400,8 @@ class BaseCompanyListAPIView(APIView):
|
||||
for c in companies
|
||||
]
|
||||
|
||||
return Response({
|
||||
"results": data,
|
||||
"count": len(data)
|
||||
})
|
||||
return Response({"results": data, "count": len(data)})
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary="List manufacturers",
|
||||
@@ -2462,6 +2412,7 @@ class BaseCompanyListAPIView(APIView):
|
||||
class ManufacturerListAPIView(BaseCompanyListAPIView):
|
||||
role = "MANUFACTURER"
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary="List designers",
|
||||
description="List all companies with DESIGNER role.",
|
||||
|
||||
Reference in New Issue
Block a user