mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 16:11:08 -05:00
Last with the old frontend
This commit is contained in:
2
backend/.gitattributes
vendored
Normal file
2
backend/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# SCM syntax highlighting & preventing 3-way merges
|
||||||
|
pixi.lock merge=binary linguist-language=YAML linguist-generated=true
|
||||||
3
backend/.gitignore
vendored
Normal file
3
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# pixi environments
|
||||||
|
.pixi/*
|
||||||
|
!.pixi/config.toml
|
||||||
362
backend/apps/api/v1/parks/company_views.py
Normal file
362
backend/apps/api/v1/parks/company_views.py
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
"""
|
||||||
|
Parks Company API views for ThrillWiki API v1.
|
||||||
|
|
||||||
|
This module implements comprehensive Company endpoints for the Parks domain,
|
||||||
|
handling companies with OPERATOR and PROPERTY_OWNER roles.
|
||||||
|
|
||||||
|
Endpoints:
|
||||||
|
- List / Create: GET /parks/companies/ POST /parks/companies/
|
||||||
|
- Retrieve / Update / Delete: GET /parks/companies/{pk}/ PATCH/PUT/DELETE
|
||||||
|
- Search: GET /parks/companies/search/?q=...
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from rest_framework import status, permissions
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.pagination import PageNumberPagination
|
||||||
|
from rest_framework.exceptions import NotFound
|
||||||
|
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
|
||||||
|
# Import serializers
|
||||||
|
from apps.api.v1.serializers.companies import (
|
||||||
|
CompanyDetailOutputSerializer,
|
||||||
|
CompanyCreateInputSerializer,
|
||||||
|
CompanyUpdateInputSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attempt to import model-level helpers; fall back gracefully if not present.
|
||||||
|
try:
|
||||||
|
from apps.parks.models import Company as ParkCompany # type: ignore
|
||||||
|
|
||||||
|
MODELS_AVAILABLE = True
|
||||||
|
except Exception:
|
||||||
|
ParkCompany = None # type: ignore
|
||||||
|
MODELS_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
class StandardResultsSetPagination(PageNumberPagination):
|
||||||
|
page_size = 20
|
||||||
|
page_size_query_param = "page_size"
|
||||||
|
max_page_size = 1000
|
||||||
|
|
||||||
|
|
||||||
|
# --- Company list & create -------------------------------------------------
|
||||||
|
class ParkCompanyListCreateAPIView(APIView):
|
||||||
|
"""
|
||||||
|
Parks Company endpoints for OPERATOR and PROPERTY_OWNER companies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary="List park companies (operators/property owners)",
|
||||||
|
description=(
|
||||||
|
"List companies with OPERATOR and PROPERTY_OWNER roles "
|
||||||
|
"with filtering and pagination."
|
||||||
|
),
|
||||||
|
parameters=[
|
||||||
|
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="roles",
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
|
type=OpenApiTypes.STR,
|
||||||
|
description=(
|
||||||
|
"Filter by roles: OPERATOR, PROPERTY_OWNER (comma-separated)"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses={200: CompanyDetailOutputSerializer(many=True)},
|
||||||
|
tags=["Parks", "Companies"],
|
||||||
|
)
|
||||||
|
def get(self, request: Request) -> Response:
|
||||||
|
"""List park companies with filtering and pagination."""
|
||||||
|
if not MODELS_AVAILABLE:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"detail": (
|
||||||
|
"Park company listing is not available because domain models "
|
||||||
|
"are not imported. Implement apps.parks.models.Company "
|
||||||
|
"to enable listing."
|
||||||
|
)
|
||||||
|
},
|
||||||
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter to only park-related roles
|
||||||
|
qs = ParkCompany.objects.filter(
|
||||||
|
roles__overlap=["OPERATOR", "PROPERTY_OWNER"]
|
||||||
|
).distinct() # type: ignore
|
||||||
|
|
||||||
|
# Basic filters
|
||||||
|
q = request.query_params.get("search")
|
||||||
|
if q:
|
||||||
|
qs = qs.filter(name__icontains=q)
|
||||||
|
|
||||||
|
roles = request.query_params.get("roles")
|
||||||
|
if roles:
|
||||||
|
role_list = [role.strip().upper() for role in roles.split(",")]
|
||||||
|
# Filter to companies that have any of the specified roles
|
||||||
|
valid_roles = [r for r in role_list if r in ["OPERATOR", "PROPERTY_OWNER"]]
|
||||||
|
if valid_roles:
|
||||||
|
qs = qs.filter(roles__overlap=valid_roles)
|
||||||
|
|
||||||
|
paginator = StandardResultsSetPagination()
|
||||||
|
page = paginator.paginate_queryset(qs, request)
|
||||||
|
serializer = CompanyDetailOutputSerializer(
|
||||||
|
page, many=True, context={"request": request}
|
||||||
|
)
|
||||||
|
return paginator.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary="Create a new park company",
|
||||||
|
description="Create a new company with OPERATOR and/or PROPERTY_OWNER roles.",
|
||||||
|
request=CompanyCreateInputSerializer,
|
||||||
|
responses={201: CompanyDetailOutputSerializer()},
|
||||||
|
tags=["Parks", "Companies"],
|
||||||
|
)
|
||||||
|
def post(self, request: Request) -> Response:
|
||||||
|
"""Create a new park company."""
|
||||||
|
if not MODELS_AVAILABLE:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"detail": (
|
||||||
|
"Park company creation is not available because domain models "
|
||||||
|
"are not imported. Implement apps.parks.models.Company "
|
||||||
|
"and necessary create logic."
|
||||||
|
)
|
||||||
|
},
|
||||||
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer_in = CompanyCreateInputSerializer(data=request.data)
|
||||||
|
serializer_in.is_valid(raise_exception=True)
|
||||||
|
validated = serializer_in.validated_data
|
||||||
|
|
||||||
|
# Validate that roles are appropriate for parks domain
|
||||||
|
roles = validated.get("roles", [])
|
||||||
|
valid_park_roles = [r for r in roles if r in ["OPERATOR", "PROPERTY_OWNER"]]
|
||||||
|
if not valid_park_roles:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"detail": (
|
||||||
|
"Park companies must have at least one of: "
|
||||||
|
"OPERATOR, PROPERTY_OWNER"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create the company
|
||||||
|
company = ParkCompany.objects.create( # type: ignore
|
||||||
|
name=validated["name"],
|
||||||
|
roles=valid_park_roles,
|
||||||
|
description=validated.get("description", ""),
|
||||||
|
website=validated.get("website", ""),
|
||||||
|
founded_date=validated.get("founded_date"),
|
||||||
|
)
|
||||||
|
|
||||||
|
out_serializer = CompanyDetailOutputSerializer(
|
||||||
|
company, context={"request": request}
|
||||||
|
)
|
||||||
|
return Response(out_serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Company retrieve / update / delete ------------------------------------
|
||||||
|
@extend_schema(
|
||||||
|
summary="Retrieve, update or delete a park company",
|
||||||
|
responses={200: CompanyDetailOutputSerializer()},
|
||||||
|
tags=["Parks", "Companies"],
|
||||||
|
)
|
||||||
|
class ParkCompanyDetailAPIView(APIView):
|
||||||
|
"""
|
||||||
|
Park Company detail endpoints for OPERATOR and PROPERTY_OWNER companies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
def _get_company_or_404(self, pk: int) -> Any:
|
||||||
|
if not MODELS_AVAILABLE:
|
||||||
|
raise NotFound(
|
||||||
|
(
|
||||||
|
"Park company detail is not available because domain models "
|
||||||
|
"are not imported. Implement apps.parks.models.Company "
|
||||||
|
"to enable detail endpoints."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# Only allow access to companies with park-related roles
|
||||||
|
return ParkCompany.objects.filter(
|
||||||
|
roles__overlap=["OPERATOR", "PROPERTY_OWNER"]
|
||||||
|
).get(
|
||||||
|
pk=pk
|
||||||
|
) # type: ignore
|
||||||
|
except ParkCompany.DoesNotExist: # type: ignore
|
||||||
|
raise NotFound("Park company not found")
|
||||||
|
|
||||||
|
def get(self, request: Request, pk: int) -> Response:
|
||||||
|
"""Retrieve a park company."""
|
||||||
|
company = self._get_company_or_404(pk)
|
||||||
|
serializer = CompanyDetailOutputSerializer(
|
||||||
|
company, context={"request": request}
|
||||||
|
)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
request=CompanyUpdateInputSerializer,
|
||||||
|
responses={200: CompanyDetailOutputSerializer()},
|
||||||
|
)
|
||||||
|
def patch(self, request: Request, pk: int) -> Response:
|
||||||
|
"""Update a park company."""
|
||||||
|
company = self._get_company_or_404(pk)
|
||||||
|
|
||||||
|
serializer_in = CompanyUpdateInputSerializer(data=request.data, partial=True)
|
||||||
|
serializer_in.is_valid(raise_exception=True)
|
||||||
|
validated = serializer_in.validated_data
|
||||||
|
|
||||||
|
# If roles are being updated, validate they're appropriate for parks domain
|
||||||
|
if "roles" in validated:
|
||||||
|
roles = validated["roles"]
|
||||||
|
valid_park_roles = [r for r in roles if r in ["OPERATOR", "PROPERTY_OWNER"]]
|
||||||
|
if not valid_park_roles:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"detail": (
|
||||||
|
"Park companies must have at least one of: "
|
||||||
|
"OPERATOR, PROPERTY_OWNER"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
validated["roles"] = valid_park_roles
|
||||||
|
|
||||||
|
if not MODELS_AVAILABLE:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"detail": (
|
||||||
|
"Park company update is not available because domain models "
|
||||||
|
"are not imported."
|
||||||
|
)
|
||||||
|
},
|
||||||
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||||
|
)
|
||||||
|
|
||||||
|
for key, value in validated.items():
|
||||||
|
setattr(company, key, value)
|
||||||
|
company.save()
|
||||||
|
|
||||||
|
serializer = CompanyDetailOutputSerializer(
|
||||||
|
company, context={"request": request}
|
||||||
|
)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
def put(self, request: Request, pk: int) -> Response:
|
||||||
|
"""Full replace - reuse patch behavior for simplicity."""
|
||||||
|
return self.patch(request, pk)
|
||||||
|
|
||||||
|
def delete(self, request: Request, pk: int) -> Response:
|
||||||
|
"""Delete a park company."""
|
||||||
|
if not MODELS_AVAILABLE:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"detail": (
|
||||||
|
"Park company delete is not available because domain models "
|
||||||
|
"are not imported."
|
||||||
|
)
|
||||||
|
},
|
||||||
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||||
|
)
|
||||||
|
company = self._get_company_or_404(pk)
|
||||||
|
company.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Company search (enhanced) ---------------------------------------------
|
||||||
|
@extend_schema(
|
||||||
|
summary="Search park companies (operators/property owners) for autocomplete",
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
||||||
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="roles",
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
|
type=OpenApiTypes.STR,
|
||||||
|
description="Filter by roles: OPERATOR, PROPERTY_OWNER (comma-separated)",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses={200: OpenApiTypes.OBJECT},
|
||||||
|
tags=["Parks", "Companies"],
|
||||||
|
)
|
||||||
|
class ParkCompanySearchAPIView(APIView):
|
||||||
|
"""
|
||||||
|
Enhanced park company search with role filtering.
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
def get(self, request: Request) -> Response:
|
||||||
|
q = request.query_params.get("q", "")
|
||||||
|
if not q:
|
||||||
|
return Response([], status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
if ParkCompany is None:
|
||||||
|
# Provide helpful placeholder structure
|
||||||
|
return Response(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Six Flags Entertainment",
|
||||||
|
"slug": "six-flags",
|
||||||
|
"roles": ["OPERATOR"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Cedar Fair",
|
||||||
|
"slug": "cedar-fair",
|
||||||
|
"roles": ["OPERATOR", "PROPERTY_OWNER"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Disney Parks",
|
||||||
|
"slug": "disney",
|
||||||
|
"roles": ["OPERATOR", "PROPERTY_OWNER"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter to only park-related roles
|
||||||
|
qs = ParkCompany.objects.filter(
|
||||||
|
name__icontains=q, roles__overlap=["OPERATOR", "PROPERTY_OWNER"]
|
||||||
|
).distinct() # type: ignore
|
||||||
|
|
||||||
|
# Additional role filtering
|
||||||
|
roles = request.query_params.get("roles")
|
||||||
|
if roles:
|
||||||
|
role_list = [role.strip().upper() for role in roles.split(",")]
|
||||||
|
valid_roles = [r for r in role_list if r in ["OPERATOR", "PROPERTY_OWNER"]]
|
||||||
|
if valid_roles:
|
||||||
|
qs = qs.filter(roles__overlap=valid_roles)
|
||||||
|
|
||||||
|
qs = qs[:20] # Limit results
|
||||||
|
results = [
|
||||||
|
{
|
||||||
|
"id": c.id,
|
||||||
|
"name": c.name,
|
||||||
|
"slug": getattr(c, "slug", ""),
|
||||||
|
"roles": c.roles if hasattr(c, "roles") else [],
|
||||||
|
}
|
||||||
|
for c in qs
|
||||||
|
]
|
||||||
|
return Response(results)
|
||||||
@@ -13,9 +13,13 @@ from .park_views import (
|
|||||||
ParkListCreateAPIView,
|
ParkListCreateAPIView,
|
||||||
ParkDetailAPIView,
|
ParkDetailAPIView,
|
||||||
FilterOptionsAPIView,
|
FilterOptionsAPIView,
|
||||||
CompanySearchAPIView,
|
|
||||||
ParkSearchSuggestionsAPIView,
|
ParkSearchSuggestionsAPIView,
|
||||||
)
|
)
|
||||||
|
from .company_views import (
|
||||||
|
ParkCompanyListCreateAPIView,
|
||||||
|
ParkCompanyDetailAPIView,
|
||||||
|
ParkCompanySearchAPIView,
|
||||||
|
)
|
||||||
from .views import ParkPhotoViewSet
|
from .views import ParkPhotoViewSet
|
||||||
|
|
||||||
# Create router for nested photo endpoints
|
# Create router for nested photo endpoints
|
||||||
@@ -29,10 +33,13 @@ urlpatterns = [
|
|||||||
path("", ParkListCreateAPIView.as_view(), name="park-list-create"),
|
path("", ParkListCreateAPIView.as_view(), name="park-list-create"),
|
||||||
# Filter options
|
# Filter options
|
||||||
path("filter-options/", FilterOptionsAPIView.as_view(), name="park-filter-options"),
|
path("filter-options/", FilterOptionsAPIView.as_view(), name="park-filter-options"),
|
||||||
|
# Company endpoints - domain-specific CRUD for OPERATOR/PROPERTY_OWNER companies
|
||||||
|
path("companies/", ParkCompanyListCreateAPIView.as_view(), name="park-companies-list-create"),
|
||||||
|
path("companies/<int:pk>/", ParkCompanyDetailAPIView.as_view(), name="park-company-detail"),
|
||||||
# Autocomplete / suggestion endpoints
|
# Autocomplete / suggestion endpoints
|
||||||
path(
|
path(
|
||||||
"search/companies/",
|
"search/companies/",
|
||||||
CompanySearchAPIView.as_view(),
|
ParkCompanySearchAPIView.as_view(),
|
||||||
name="park-search-companies",
|
name="park-search-companies",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
|
|||||||
352
backend/apps/api/v1/rides/company_views.py
Normal file
352
backend/apps/api/v1/rides/company_views.py
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
"""
|
||||||
|
Rides Company API views for ThrillWiki API v1.
|
||||||
|
|
||||||
|
This module implements comprehensive Company CRUD endpoints specifically for the
|
||||||
|
Rides domain:
|
||||||
|
- Companies with MANUFACTURER and DESIGNER roles
|
||||||
|
- List / Create: GET /rides/companies/ POST /rides/companies/
|
||||||
|
- Retrieve / Update / Delete: GET /rides/companies/{pk}/ PATCH/PUT/DELETE
|
||||||
|
/rides/companies/{pk}/
|
||||||
|
- Enhanced search: GET /rides/search/companies/?q=...&role=...
|
||||||
|
|
||||||
|
These views handle companies that manufacture or design rides, staying within the
|
||||||
|
Rides domain boundary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from rest_framework import status, permissions
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.pagination import PageNumberPagination
|
||||||
|
from rest_framework.exceptions import NotFound, ValidationError
|
||||||
|
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
|
||||||
|
# Reuse existing Company serializers
|
||||||
|
from apps.api.v1.serializers.companies import (
|
||||||
|
CompanyDetailOutputSerializer,
|
||||||
|
CompanyCreateInputSerializer,
|
||||||
|
CompanyUpdateInputSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attempt to import Rides Company model; fall back gracefully if not present
|
||||||
|
try:
|
||||||
|
from apps.rides.models import Company as RideCompany # type: ignore
|
||||||
|
|
||||||
|
MODELS_AVAILABLE = True
|
||||||
|
except Exception:
|
||||||
|
RideCompany = None # type: ignore
|
||||||
|
MODELS_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
class StandardResultsSetPagination(PageNumberPagination):
|
||||||
|
page_size = 20
|
||||||
|
page_size_query_param = "page_size"
|
||||||
|
max_page_size = 1000
|
||||||
|
|
||||||
|
|
||||||
|
# --- Company list & create for Rides domain --------------------------------
|
||||||
|
class RideCompanyListCreateAPIView(APIView):
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary="List ride companies with filtering and pagination",
|
||||||
|
description=(
|
||||||
|
"List companies with MANUFACTURER and DESIGNER roles for the rides domain."
|
||||||
|
),
|
||||||
|
parameters=[
|
||||||
|
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,
|
||||||
|
description="Search companies by name",
|
||||||
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="role",
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
|
type=OpenApiTypes.STR,
|
||||||
|
description="Filter by role: MANUFACTURER, DESIGNER",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses={200: CompanyDetailOutputSerializer(many=True)},
|
||||||
|
tags=["Rides"],
|
||||||
|
)
|
||||||
|
def get(self, request: Request) -> Response:
|
||||||
|
"""List ride companies with basic filtering and pagination."""
|
||||||
|
if not MODELS_AVAILABLE:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"detail": (
|
||||||
|
"Ride company listing is not available because domain "
|
||||||
|
"models are not imported. Implement "
|
||||||
|
"apps.rides.models.Company to enable listing."
|
||||||
|
)
|
||||||
|
},
|
||||||
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter to only ride-related roles
|
||||||
|
qs = RideCompany.objects.filter( # type: ignore
|
||||||
|
roles__overlap=["MANUFACTURER", "DESIGNER"]
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
# Basic filters
|
||||||
|
search_query = request.query_params.get("search")
|
||||||
|
if search_query:
|
||||||
|
qs = qs.filter(name__icontains=search_query)
|
||||||
|
|
||||||
|
role_filter = request.query_params.get("role")
|
||||||
|
if role_filter and role_filter in ["MANUFACTURER", "DESIGNER"]:
|
||||||
|
qs = qs.filter(roles__contains=[role_filter])
|
||||||
|
|
||||||
|
paginator = StandardResultsSetPagination()
|
||||||
|
page = paginator.paginate_queryset(qs, request)
|
||||||
|
serializer = CompanyDetailOutputSerializer(
|
||||||
|
page, many=True, context={"request": request}
|
||||||
|
)
|
||||||
|
return paginator.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary="Create a new ride company",
|
||||||
|
description=(
|
||||||
|
"Create a new company with MANUFACTURER and/or DESIGNER roles "
|
||||||
|
"for the rides domain."
|
||||||
|
),
|
||||||
|
request=CompanyCreateInputSerializer,
|
||||||
|
responses={201: CompanyDetailOutputSerializer()},
|
||||||
|
tags=["Rides"],
|
||||||
|
)
|
||||||
|
def post(self, request: Request) -> Response:
|
||||||
|
"""Create a new ride company."""
|
||||||
|
if not MODELS_AVAILABLE:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"detail": (
|
||||||
|
"Ride company creation is not available because domain "
|
||||||
|
"models are not imported. Implement "
|
||||||
|
"apps.rides.models.Company to enable creation."
|
||||||
|
)
|
||||||
|
},
|
||||||
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer_in = CompanyCreateInputSerializer(data=request.data)
|
||||||
|
serializer_in.is_valid(raise_exception=True)
|
||||||
|
validated = serializer_in.validated_data
|
||||||
|
|
||||||
|
# Validate roles for rides domain
|
||||||
|
roles = validated.get("roles", [])
|
||||||
|
valid_ride_roles = [
|
||||||
|
role for role in roles if role in ["MANUFACTURER", "DESIGNER"]
|
||||||
|
]
|
||||||
|
|
||||||
|
if not valid_ride_roles:
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"roles": (
|
||||||
|
"At least one role must be MANUFACTURER or DESIGNER "
|
||||||
|
"for ride companies."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only keep valid ride roles
|
||||||
|
if len(valid_ride_roles) != len(roles):
|
||||||
|
validated["roles"] = valid_ride_roles
|
||||||
|
|
||||||
|
# Create the company
|
||||||
|
company = RideCompany.objects.create( # type: ignore
|
||||||
|
name=validated["name"],
|
||||||
|
slug=validated.get("slug", ""),
|
||||||
|
roles=validated["roles"],
|
||||||
|
description=validated.get("description", ""),
|
||||||
|
website=validated.get("website", ""),
|
||||||
|
founded_date=validated.get("founded_date"),
|
||||||
|
)
|
||||||
|
|
||||||
|
out_serializer = CompanyDetailOutputSerializer(
|
||||||
|
company, context={"request": request}
|
||||||
|
)
|
||||||
|
return Response(out_serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Company retrieve / update / delete ------------------------------------
|
||||||
|
class RideCompanyDetailAPIView(APIView):
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
def _get_company_or_404(self, pk: int) -> Any:
|
||||||
|
if not MODELS_AVAILABLE:
|
||||||
|
raise NotFound(
|
||||||
|
(
|
||||||
|
"Ride company detail is not available because domain models "
|
||||||
|
"are not imported. Implement apps.rides.models.Company to "
|
||||||
|
"enable detail endpoints."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
return RideCompany.objects.filter(
|
||||||
|
roles__overlap=["MANUFACTURER", "DESIGNER"]
|
||||||
|
).get(pk=pk)
|
||||||
|
except RideCompany.DoesNotExist:
|
||||||
|
raise NotFound("Ride company not found")
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary="Retrieve a ride company",
|
||||||
|
responses={200: CompanyDetailOutputSerializer()},
|
||||||
|
tags=["Rides"],
|
||||||
|
)
|
||||||
|
def get(self, request: Request, pk: int) -> Response:
|
||||||
|
"""Retrieve a ride company."""
|
||||||
|
company = self._get_company_or_404(pk)
|
||||||
|
serializer = CompanyDetailOutputSerializer(
|
||||||
|
company, context={"request": request}
|
||||||
|
)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
request=CompanyUpdateInputSerializer,
|
||||||
|
responses={200: CompanyDetailOutputSerializer()},
|
||||||
|
tags=["Rides"],
|
||||||
|
)
|
||||||
|
def patch(self, request: Request, pk: int) -> Response:
|
||||||
|
"""Update a ride company."""
|
||||||
|
company = self._get_company_or_404(pk)
|
||||||
|
|
||||||
|
serializer_in = CompanyUpdateInputSerializer(data=request.data, partial=True)
|
||||||
|
serializer_in.is_valid(raise_exception=True)
|
||||||
|
validated = serializer_in.validated_data
|
||||||
|
|
||||||
|
# Validate roles for rides domain if being updated
|
||||||
|
if "roles" in validated:
|
||||||
|
roles = validated["roles"]
|
||||||
|
valid_ride_roles = [
|
||||||
|
role for role in roles if role in ["MANUFACTURER", "DESIGNER"]
|
||||||
|
]
|
||||||
|
|
||||||
|
if not valid_ride_roles:
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"roles": (
|
||||||
|
"At least one role must be MANUFACTURER or DESIGNER "
|
||||||
|
"for ride companies."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only keep valid ride roles
|
||||||
|
validated["roles"] = valid_ride_roles
|
||||||
|
|
||||||
|
# Update the company
|
||||||
|
for key, value in validated.items():
|
||||||
|
setattr(company, key, value)
|
||||||
|
company.save()
|
||||||
|
|
||||||
|
serializer = CompanyDetailOutputSerializer(
|
||||||
|
company, context={"request": request}
|
||||||
|
)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
def put(self, request: Request, pk: int) -> Response:
|
||||||
|
"""Full replace - reuse patch behavior for simplicity."""
|
||||||
|
return self.patch(request, pk)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary="Delete a ride company",
|
||||||
|
responses={204: None},
|
||||||
|
tags=["Rides"],
|
||||||
|
)
|
||||||
|
def delete(self, request: Request, pk: int) -> Response:
|
||||||
|
"""Delete a ride company."""
|
||||||
|
company = self._get_company_or_404(pk)
|
||||||
|
company.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Enhanced Company search (autocomplete) for Rides domain ---------------
|
||||||
|
class RideCompanySearchAPIView(APIView):
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary="Search ride companies (manufacturers/designers) for autocomplete",
|
||||||
|
description=(
|
||||||
|
"Enhanced search for companies with MANUFACTURER and DESIGNER roles."
|
||||||
|
),
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="q",
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
|
type=OpenApiTypes.STR,
|
||||||
|
description="Search query for company names",
|
||||||
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="role",
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
|
type=OpenApiTypes.STR,
|
||||||
|
description="Filter by specific role: MANUFACTURER, DESIGNER",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses={200: OpenApiTypes.OBJECT},
|
||||||
|
tags=["Rides"],
|
||||||
|
)
|
||||||
|
def get(self, request: Request) -> Response:
|
||||||
|
"""Enhanced search for ride companies with role filtering."""
|
||||||
|
q = request.query_params.get("q", "")
|
||||||
|
role_filter = request.query_params.get("role", "")
|
||||||
|
|
||||||
|
if not q:
|
||||||
|
return Response([], status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
if RideCompany is None:
|
||||||
|
# Provide helpful placeholder structure
|
||||||
|
return Response(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Rocky Mountain Construction",
|
||||||
|
"slug": "rmc",
|
||||||
|
"roles": ["MANUFACTURER"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Bolliger & Mabillard",
|
||||||
|
"slug": "b&m",
|
||||||
|
"roles": ["MANUFACTURER"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Alan Schilke",
|
||||||
|
"slug": "alan-schilke",
|
||||||
|
"roles": ["DESIGNER"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter to only ride-related roles
|
||||||
|
qs = RideCompany.objects.filter(
|
||||||
|
name__icontains=q, roles__overlap=["MANUFACTURER", "DESIGNER"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply role filter if specified
|
||||||
|
if role_filter and role_filter in ["MANUFACTURER", "DESIGNER"]:
|
||||||
|
qs = qs.filter(roles__contains=[role_filter])
|
||||||
|
|
||||||
|
qs = qs[:20] # Limit results
|
||||||
|
|
||||||
|
results = [
|
||||||
|
{
|
||||||
|
"id": c.id,
|
||||||
|
"name": c.name,
|
||||||
|
"slug": getattr(c, "slug", ""),
|
||||||
|
"roles": c.roles if hasattr(c, "roles") else [],
|
||||||
|
}
|
||||||
|
for c in qs
|
||||||
|
]
|
||||||
|
return Response(results)
|
||||||
@@ -15,10 +15,14 @@ from .views import (
|
|||||||
RideListCreateAPIView,
|
RideListCreateAPIView,
|
||||||
RideDetailAPIView,
|
RideDetailAPIView,
|
||||||
FilterOptionsAPIView,
|
FilterOptionsAPIView,
|
||||||
CompanySearchAPIView,
|
|
||||||
RideModelSearchAPIView,
|
RideModelSearchAPIView,
|
||||||
RideSearchSuggestionsAPIView,
|
RideSearchSuggestionsAPIView,
|
||||||
)
|
)
|
||||||
|
from .company_views import (
|
||||||
|
RideCompanyListCreateAPIView,
|
||||||
|
RideCompanyDetailAPIView,
|
||||||
|
RideCompanySearchAPIView,
|
||||||
|
)
|
||||||
from .photo_views import RidePhotoViewSet
|
from .photo_views import RidePhotoViewSet
|
||||||
|
|
||||||
# Create router for nested photo endpoints
|
# Create router for nested photo endpoints
|
||||||
@@ -32,10 +36,15 @@ urlpatterns = [
|
|||||||
path("", RideListCreateAPIView.as_view(), name="ride-list-create"),
|
path("", RideListCreateAPIView.as_view(), name="ride-list-create"),
|
||||||
# Filter options
|
# Filter options
|
||||||
path("filter-options/", FilterOptionsAPIView.as_view(), name="ride-filter-options"),
|
path("filter-options/", FilterOptionsAPIView.as_view(), name="ride-filter-options"),
|
||||||
|
# Company endpoints - domain-specific CRUD for MANUFACTURER/DESIGNER companies
|
||||||
|
path("companies/", RideCompanyListCreateAPIView.as_view(),
|
||||||
|
name="ride-companies-list-create"),
|
||||||
|
path("companies/<int:pk>/", RideCompanyDetailAPIView.as_view(),
|
||||||
|
name="ride-company-detail"),
|
||||||
# Autocomplete / suggestion endpoints
|
# Autocomplete / suggestion endpoints
|
||||||
path(
|
path(
|
||||||
"search/companies/",
|
"search/companies/",
|
||||||
CompanySearchAPIView.as_view(),
|
RideCompanySearchAPIView.as_view(),
|
||||||
name="ride-search-companies",
|
name="ride-search-companies",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class HealthCheckAPIView(APIView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Process individual health checks
|
# Process individual health checks
|
||||||
for plugin in plugins:
|
for plugin in plugins.values():
|
||||||
plugin_name = plugin.identifier()
|
plugin_name = plugin.identifier()
|
||||||
plugin_errors = (
|
plugin_errors = (
|
||||||
errors.get(plugin.__class__.__name__, [])
|
errors.get(plugin.__class__.__name__, [])
|
||||||
@@ -127,7 +127,7 @@ class HealthCheckAPIView(APIView):
|
|||||||
# Check if any critical services are failing
|
# Check if any critical services are failing
|
||||||
critical_errors = any(
|
critical_errors = any(
|
||||||
getattr(plugin, "critical_service", False)
|
getattr(plugin, "critical_service", False)
|
||||||
for plugin in plugins
|
for plugin in plugins.values()
|
||||||
if isinstance(errors, dict) and errors.get(plugin.__class__.__name__)
|
if isinstance(errors, dict) and errors.get(plugin.__class__.__name__)
|
||||||
)
|
)
|
||||||
status_code = 503 if critical_errors else 200
|
status_code = 503 if critical_errors else 200
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ Local development settings for thrillwiki project.
|
|||||||
|
|
||||||
from ..settings import database
|
from ..settings import database
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
from decouple import config
|
||||||
|
import re
|
||||||
from .base import (
|
from .base import (
|
||||||
BASE_DIR,
|
BASE_DIR,
|
||||||
INSTALLED_APPS,
|
INSTALLED_APPS,
|
||||||
@@ -45,6 +48,31 @@ CSRF_TRUSTED_ORIGINS = [
|
|||||||
"https://beta.thrillwiki.com",
|
"https://beta.thrillwiki.com",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CORS_ALLOWED_ORIGIN_REGEXES = [
|
||||||
|
# Matches http://localhost:3000, http://localhost:3001, etc.
|
||||||
|
r"^http://localhost:\d+$",
|
||||||
|
# Matches http://127.0.0.1:3000, http://127.0.0.1:8080, etc.
|
||||||
|
r"^http://127\.0\.0\.1:\d+$",
|
||||||
|
]
|
||||||
|
|
||||||
|
CORS_ALLOW_HEADERS = [
|
||||||
|
'accept',
|
||||||
|
'accept-encoding',
|
||||||
|
'authorization',
|
||||||
|
'content-type',
|
||||||
|
'dnt',
|
||||||
|
'origin',
|
||||||
|
'user-agent',
|
||||||
|
'x-csrftoken',
|
||||||
|
'x-requested-with',
|
||||||
|
'x-nextjs-data', # Next.js specific header
|
||||||
|
]
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = True # ⚠️ Only for development!
|
||||||
|
else:
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = False
|
||||||
|
|
||||||
GDAL_LIBRARY_PATH = "/opt/homebrew/lib/libgdal.dylib"
|
GDAL_LIBRARY_PATH = "/opt/homebrew/lib/libgdal.dylib"
|
||||||
GEOS_LIBRARY_PATH = "/opt/homebrew/lib/libgeos_c.dylib"
|
GEOS_LIBRARY_PATH = "/opt/homebrew/lib/libgeos_c.dylib"
|
||||||
|
|
||||||
|
|||||||
2652
backend/pixi.lock
generated
Normal file
2652
backend/pixi.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -77,3 +77,16 @@ stubPath = "stubs"
|
|||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
python-json-logger = { url = "https://github.com/nhairs/python-json-logger/releases/download/v3.0.0/python_json_logger-3.0.0-py3-none-any.whl" }
|
python-json-logger = { url = "https://github.com/nhairs/python-json-logger/releases/download/v3.0.0/python_json_logger-3.0.0-py3-none-any.whl" }
|
||||||
|
|
||||||
|
[tool.pixi.workspace]
|
||||||
|
channels = ["conda-forge"]
|
||||||
|
platforms = ["osx-arm64"]
|
||||||
|
|
||||||
|
[tool.pixi.pypi-dependencies]
|
||||||
|
thrillwiki = { path = ".", editable = true }
|
||||||
|
|
||||||
|
[tool.pixi.environments]
|
||||||
|
default = { solve-group = "default" }
|
||||||
|
dev = { features = ["dev"], solve-group = "default" }
|
||||||
|
|
||||||
|
[tool.pixi.tasks]
|
||||||
|
|||||||
Reference in New Issue
Block a user