mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 02:15:19 -05:00
feat(accounts): add public profiles list endpoint with search and pagination
- Add new `/profiles/` endpoint for listing user profiles with search, filtering, and pagination support - Implement list_profiles view with OpenAPI documentation for user discovery and leaderboards - Refactor WebAuthn authentication state management to simplify begin_authentication flow - Update MFA passkey login to store user reference instead of full state in cache This enables public profile browsing and improves the passkey authentication implementation by leveraging allauth's internal session management.
This commit is contained in:
254
backend/apps/api/v1/rides/ride_model_views.py
Normal file
254
backend/apps/api/v1/rides/ride_model_views.py
Normal file
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
Global Ride Model views for ThrillWiki API v1.
|
||||
|
||||
This module provides top-level ride model endpoints that don't require
|
||||
manufacturer context, matching the frontend's expectation of /rides/models/.
|
||||
"""
|
||||
|
||||
from django.db.models import Q
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
# Import serializers
|
||||
from apps.api.v1.serializers.ride_models import (
|
||||
RideModelDetailOutputSerializer,
|
||||
RideModelListOutputSerializer,
|
||||
)
|
||||
|
||||
# Attempt to import models
|
||||
try:
|
||||
from apps.rides.models import RideModel
|
||||
from apps.rides.models.company import Company
|
||||
|
||||
MODELS_AVAILABLE = True
|
||||
except ImportError:
|
||||
try:
|
||||
from apps.rides.models.rides import Company, RideModel
|
||||
|
||||
MODELS_AVAILABLE = True
|
||||
except ImportError:
|
||||
RideModel = None
|
||||
Company = None
|
||||
MODELS_AVAILABLE = False
|
||||
|
||||
|
||||
class StandardResultsSetPagination(PageNumberPagination):
|
||||
page_size = 20
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 100
|
||||
|
||||
|
||||
class GlobalRideModelListAPIView(APIView):
|
||||
"""
|
||||
Global ride model list endpoint.
|
||||
|
||||
This endpoint provides a top-level list of all ride models without
|
||||
requiring a manufacturer slug, matching the frontend's expectation
|
||||
of calling /rides/models/ directly.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
summary="List all ride models with filtering and pagination",
|
||||
description=(
|
||||
"List all ride models across all manufacturers with comprehensive "
|
||||
"filtering and pagination support. This is a global endpoint that "
|
||||
"doesn't require manufacturer context."
|
||||
),
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="page",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.INT,
|
||||
description="Page number for pagination",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="page_size",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.INT,
|
||||
description="Number of results per page (max 100)",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="search",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
description="Search term for name, description, or manufacturer",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="category",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
description="Filter by category (e.g., RC, DR, FR, WR)",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="manufacturer",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
description="Filter by manufacturer slug",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="target_market",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
description="Filter by target market (e.g., FAMILY, THRILL)",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="is_discontinued",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.BOOL,
|
||||
description="Filter by discontinued status",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="ordering",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
description="Order by field: name, -name, manufacturer__name, etc.",
|
||||
),
|
||||
],
|
||||
responses={200: RideModelListOutputSerializer(many=True)},
|
||||
tags=["Ride Models"],
|
||||
)
|
||||
def get(self, request: Request) -> Response:
|
||||
"""List all ride models with filtering and pagination."""
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
"detail": "Ride model listing is not available.",
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
# Base queryset with eager loading
|
||||
qs = RideModel.objects.select_related("manufacturer").prefetch_related(
|
||||
"photos"
|
||||
).order_by("manufacturer__name", "name")
|
||||
|
||||
# Search filter
|
||||
search = request.query_params.get("search", "").strip()
|
||||
if search:
|
||||
qs = qs.filter(
|
||||
Q(name__icontains=search)
|
||||
| Q(description__icontains=search)
|
||||
| Q(manufacturer__name__icontains=search)
|
||||
)
|
||||
|
||||
# Category filter
|
||||
category = request.query_params.get("category", "").strip()
|
||||
if category:
|
||||
# Support comma-separated categories
|
||||
categories = [c.strip() for c in category.split(",") if c.strip()]
|
||||
if categories:
|
||||
qs = qs.filter(category__in=categories)
|
||||
|
||||
# Manufacturer filter
|
||||
manufacturer = request.query_params.get("manufacturer", "").strip()
|
||||
if manufacturer:
|
||||
qs = qs.filter(manufacturer__slug=manufacturer)
|
||||
|
||||
# Target market filter
|
||||
target_market = request.query_params.get("target_market", "").strip()
|
||||
if target_market:
|
||||
markets = [m.strip() for m in target_market.split(",") if m.strip()]
|
||||
if markets:
|
||||
qs = qs.filter(target_market__in=markets)
|
||||
|
||||
# Discontinued filter
|
||||
is_discontinued = request.query_params.get("is_discontinued")
|
||||
if is_discontinued is not None:
|
||||
qs = qs.filter(is_discontinued=is_discontinued.lower() == "true")
|
||||
|
||||
# Ordering
|
||||
ordering = request.query_params.get("ordering", "manufacturer__name,name")
|
||||
valid_orderings = [
|
||||
"name", "-name",
|
||||
"manufacturer__name", "-manufacturer__name",
|
||||
"first_installation_year", "-first_installation_year",
|
||||
"total_installations", "-total_installations",
|
||||
"created_at", "-created_at",
|
||||
]
|
||||
if ordering:
|
||||
order_fields = [
|
||||
f.strip() for f in ordering.split(",")
|
||||
if f.strip() in valid_orderings or f.strip().lstrip("-") in [
|
||||
o.lstrip("-") for o in valid_orderings
|
||||
]
|
||||
]
|
||||
if order_fields:
|
||||
qs = qs.order_by(*order_fields)
|
||||
|
||||
# Paginate
|
||||
paginator = StandardResultsSetPagination()
|
||||
page = paginator.paginate_queryset(qs, request)
|
||||
|
||||
if page is not None:
|
||||
serializer = RideModelListOutputSerializer(
|
||||
page, many=True, context={"request": request}
|
||||
)
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
# Fallback without pagination
|
||||
serializer = RideModelListOutputSerializer(
|
||||
qs[:100], many=True, context={"request": request}
|
||||
)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class GlobalRideModelDetailAPIView(APIView):
|
||||
"""
|
||||
Global ride model detail endpoint by ID or slug.
|
||||
|
||||
This endpoint provides detail for a single ride model without
|
||||
requiring manufacturer context.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
summary="Retrieve a ride model by ID",
|
||||
description="Get detailed information about a specific ride model by its ID.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="pk",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
required=True,
|
||||
description="Ride model ID",
|
||||
),
|
||||
],
|
||||
responses={200: RideModelDetailOutputSerializer()},
|
||||
tags=["Ride Models"],
|
||||
)
|
||||
def get(self, request: Request, pk: int) -> Response:
|
||||
"""Get ride model detail by ID."""
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{"detail": "Ride model not found"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
try:
|
||||
ride_model = (
|
||||
RideModel.objects.select_related("manufacturer")
|
||||
.prefetch_related("photos", "variants", "technical_specs")
|
||||
.get(pk=pk)
|
||||
)
|
||||
except RideModel.DoesNotExist:
|
||||
return Response(
|
||||
{"detail": "Ride model not found"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
serializer = RideModelDetailOutputSerializer(
|
||||
ride_model, context={"request": request}
|
||||
)
|
||||
return Response(serializer.data)
|
||||
Reference in New Issue
Block a user