mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-01-02 01:27:03 -05:00
feat: Implement initial schema and add various API, service, and management command enhancements across the application.
This commit is contained in:
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user