mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-27 13:47:04 -05:00
feat: Introduce lists and reviews apps, refactor user list functionality from accounts, and add user profile fields.
This commit is contained in:
@@ -13,7 +13,7 @@ from apps.api.v1.serializers.accounts import (
|
||||
PrivacySettingsSerializer,
|
||||
SecuritySettingsSerializer,
|
||||
UserStatisticsSerializer,
|
||||
TopListSerializer,
|
||||
UserListSerializer,
|
||||
AccountUpdateSerializer,
|
||||
ProfileUpdateSerializer,
|
||||
ThemePreferenceSerializer,
|
||||
@@ -26,10 +26,10 @@ from apps.accounts.services import UserDeletionService
|
||||
from apps.accounts.models import (
|
||||
User,
|
||||
UserProfile,
|
||||
TopList,
|
||||
UserNotification,
|
||||
NotificationPreference,
|
||||
)
|
||||
from apps.lists.models import UserList
|
||||
import logging
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
@@ -831,7 +831,7 @@ def check_user_deletion_eligibility(request, user_id):
|
||||
user, "uploaded_ride_photos", user.__class__.objects.none()
|
||||
).count(),
|
||||
"top_lists": getattr(
|
||||
user, "top_lists", user.__class__.objects.none()
|
||||
user, "user_lists", user.__class__.objects.none()
|
||||
).count(),
|
||||
"edit_submissions": getattr(
|
||||
user, "edit_submissions", user.__class__.objects.none()
|
||||
@@ -1318,7 +1318,7 @@ def get_user_statistics(request):
|
||||
"rides_ridden": RideReview.objects.filter(user=user).values("ride").distinct().count(),
|
||||
"reviews_written": ParkReview.objects.filter(user=user).count() + RideReview.objects.filter(user=user).count(),
|
||||
"photos_uploaded": total_photos_uploaded,
|
||||
"top_lists_created": TopList.objects.filter(user=user).count(),
|
||||
"top_lists_created": UserList.objects.filter(user=user).count(),
|
||||
"member_since": user.date_joined,
|
||||
"last_activity": user.last_login,
|
||||
}
|
||||
@@ -1335,7 +1335,7 @@ def get_user_statistics(request):
|
||||
summary="Get user's top lists",
|
||||
description="Get all top lists created by the authenticated user.",
|
||||
responses={
|
||||
200: TopListSerializer(many=True),
|
||||
200: UserListSerializer(many=True),
|
||||
401: {"description": "Authentication required"},
|
||||
},
|
||||
tags=["User Content"],
|
||||
@@ -1344,8 +1344,8 @@ def get_user_statistics(request):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def get_user_top_lists(request):
|
||||
"""Get user's top lists."""
|
||||
top_lists = TopList.objects.filter(user=request.user).order_by("-created_at")
|
||||
serializer = TopListSerializer(top_lists, many=True)
|
||||
top_lists = UserList.objects.filter(user=request.user).order_by("-created_at")
|
||||
serializer = UserListSerializer(top_lists, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -1353,9 +1353,9 @@ def get_user_top_lists(request):
|
||||
operation_id="create_top_list",
|
||||
summary="Create a new top list",
|
||||
description="Create a new top list for the authenticated user.",
|
||||
request=TopListSerializer,
|
||||
request=UserListSerializer,
|
||||
responses={
|
||||
201: TopListSerializer,
|
||||
201: UserListSerializer,
|
||||
400: {"description": "Validation error"},
|
||||
},
|
||||
tags=["User Content"],
|
||||
@@ -1364,7 +1364,7 @@ def get_user_top_lists(request):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def create_top_list(request):
|
||||
"""Create a new top list."""
|
||||
serializer = TopListSerializer(data=request.data, context={"request": request})
|
||||
serializer = UserListSerializer(data=request.data, context={"request": request})
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save(user=request.user)
|
||||
@@ -1377,9 +1377,9 @@ def create_top_list(request):
|
||||
operation_id="update_top_list",
|
||||
summary="Update a top list",
|
||||
description="Update a top list owned by the authenticated user.",
|
||||
request=TopListSerializer,
|
||||
request=UserListSerializer,
|
||||
responses={
|
||||
200: TopListSerializer,
|
||||
200: UserListSerializer,
|
||||
400: {"description": "Validation error"},
|
||||
404: {"description": "Top list not found"},
|
||||
},
|
||||
@@ -1390,14 +1390,14 @@ def create_top_list(request):
|
||||
def update_top_list(request, list_id):
|
||||
"""Update a top list."""
|
||||
try:
|
||||
top_list = TopList.objects.get(id=list_id, user=request.user)
|
||||
except TopList.DoesNotExist:
|
||||
top_list = UserList.objects.get(id=list_id, user=request.user)
|
||||
except UserList.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Top list not found"},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
serializer = TopListSerializer(
|
||||
serializer = UserListSerializer(
|
||||
top_list, data=request.data, partial=True, context={"request": request}
|
||||
)
|
||||
|
||||
@@ -1423,10 +1423,10 @@ def update_top_list(request, list_id):
|
||||
def delete_top_list(request, list_id):
|
||||
"""Delete a top list."""
|
||||
try:
|
||||
top_list = TopList.objects.get(id=list_id, user=request.user)
|
||||
top_list = UserList.objects.get(id=list_id, user=request.user)
|
||||
top_list.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except TopList.DoesNotExist:
|
||||
except UserList.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Top list not found"},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
|
||||
@@ -1081,3 +1081,45 @@ class ParkImageSettingsAPIView(APIView):
|
||||
park, context={"request": request}
|
||||
)
|
||||
return Response(output_serializer.data)
|
||||
# --- Operator list ----------------------------------------------------------
|
||||
@extend_schema(
|
||||
summary="List park operators",
|
||||
description="List all companies with OPERATOR role, including park counts.",
|
||||
responses={
|
||||
200: OpenApiTypes.OBJECT,
|
||||
},
|
||||
tags=["Parks"],
|
||||
)
|
||||
class OperatorListAPIView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get(self, request: Request) -> Response:
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{"detail": "Models not available"},
|
||||
status=status.HTTP_501_NOT_IMPLEMENTED
|
||||
)
|
||||
|
||||
operators = (
|
||||
Company.objects.filter(roles__contains=["OPERATOR"])
|
||||
.annotate(park_count=Count("operated_parks"))
|
||||
.only("id", "name", "slug", "roles", "description")
|
||||
.order_by("name")
|
||||
)
|
||||
|
||||
# Simple serialization
|
||||
data = [
|
||||
{
|
||||
"id": op.id,
|
||||
"name": op.name,
|
||||
"slug": op.slug,
|
||||
"description": op.description,
|
||||
"park_count": op.park_count,
|
||||
}
|
||||
for op in operators
|
||||
]
|
||||
|
||||
return Response({
|
||||
"results": data,
|
||||
"count": len(data)
|
||||
})
|
||||
|
||||
@@ -16,15 +16,24 @@ from .park_views import (
|
||||
CompanySearchAPIView,
|
||||
ParkSearchSuggestionsAPIView,
|
||||
ParkImageSettingsAPIView,
|
||||
OperatorListAPIView,
|
||||
)
|
||||
from .park_rides_views import (
|
||||
ParkRidesListAPIView,
|
||||
ParkRideDetailAPIView,
|
||||
ParkComprehensiveDetailAPIView,
|
||||
)
|
||||
from apps.parks.views import location_search, reverse_geocode
|
||||
from .views import ParkPhotoViewSet, HybridParkAPIView, ParkFilterMetadataAPIView
|
||||
from .ride_photos_views import RidePhotoViewSet
|
||||
from .ride_photos_views import RidePhotoViewSet
|
||||
from .ride_reviews_views import RideReviewViewSet
|
||||
from apps.parks.views_roadtrip import (
|
||||
CreateTripView,
|
||||
FindParksAlongRouteView,
|
||||
GeocodeAddressView,
|
||||
ParkDistanceCalculatorView,
|
||||
)
|
||||
|
||||
# Create router for nested photo endpoints
|
||||
router = DefaultRouter()
|
||||
@@ -84,4 +93,19 @@ urlpatterns = [
|
||||
|
||||
# Nested ride review endpoints - reviews for specific rides within parks
|
||||
path("<str:park_slug>/rides/<str:ride_slug>/reviews/", include(ride_reviews_router.urls)),
|
||||
# Nested ride review endpoints - reviews for specific rides within parks
|
||||
path("<str:park_slug>/rides/<str:ride_slug>/reviews/", include(ride_reviews_router.urls)),
|
||||
|
||||
# Roadtrip API endpoints
|
||||
path("roadtrip/create/", CreateTripView.as_view(), name="roadtrip-create"),
|
||||
path("roadtrip/find-along-route/", FindParksAlongRouteView.as_view(), name="roadtrip-find"),
|
||||
path("roadtrip/geocode/", GeocodeAddressView.as_view(), name="roadtrip-geocode"),
|
||||
path("roadtrip/distance/", ParkDistanceCalculatorView.as_view(), name="roadtrip-distance"),
|
||||
|
||||
# Operator endpoints
|
||||
path("operators/", OperatorListAPIView.as_view(), name="operator-list"),
|
||||
|
||||
# Location search endpoints
|
||||
path("search/location/", location_search, name="location-search"),
|
||||
path("search/reverse-geocode/", reverse_geocode, name="reverse-geocode"),
|
||||
]
|
||||
|
||||
@@ -21,6 +21,8 @@ from .views import (
|
||||
RideImageSettingsAPIView,
|
||||
HybridRideAPIView,
|
||||
RideFilterMetadataAPIView,
|
||||
ManufacturerListAPIView,
|
||||
DesignerListAPIView,
|
||||
)
|
||||
from .photo_views import RidePhotoViewSet
|
||||
|
||||
@@ -56,6 +58,10 @@ urlpatterns = [
|
||||
RideSearchSuggestionsAPIView.as_view(),
|
||||
name="ride-search-suggestions",
|
||||
),
|
||||
# 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>/",
|
||||
|
||||
@@ -2456,3 +2456,56 @@ 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
|
||||
)
|
||||
|
||||
companies = (
|
||||
Company.objects.filter(roles__contains=[self.role])
|
||||
.annotate(ride_count=Count("manufactured_rides" if self.role == "MANUFACTURER" else "designed_rides"))
|
||||
.only("id", "name", "slug", "roles", "description")
|
||||
.order_by("name")
|
||||
)
|
||||
|
||||
data = [
|
||||
{
|
||||
"id": c.id,
|
||||
"name": c.name,
|
||||
"slug": c.slug,
|
||||
"description": c.description,
|
||||
"ride_count": c.ride_count,
|
||||
}
|
||||
for c in companies
|
||||
]
|
||||
|
||||
return Response({
|
||||
"results": data,
|
||||
"count": len(data)
|
||||
})
|
||||
|
||||
@extend_schema(
|
||||
summary="List manufacturers",
|
||||
description="List all companies with MANUFACTURER role.",
|
||||
responses={200: OpenApiTypes.OBJECT},
|
||||
tags=["Rides"],
|
||||
)
|
||||
class ManufacturerListAPIView(BaseCompanyListAPIView):
|
||||
role = "MANUFACTURER"
|
||||
|
||||
@extend_schema(
|
||||
summary="List designers",
|
||||
description="List all companies with DESIGNER role.",
|
||||
responses={200: OpenApiTypes.OBJECT},
|
||||
tags=["Rides"],
|
||||
)
|
||||
class DesignerListAPIView(BaseCompanyListAPIView):
|
||||
role = "DESIGNER"
|
||||
|
||||
@@ -14,10 +14,10 @@ from drf_spectacular.utils import (
|
||||
from apps.accounts.models import (
|
||||
User,
|
||||
UserProfile,
|
||||
TopList,
|
||||
UserNotification,
|
||||
NotificationPreference,
|
||||
)
|
||||
from apps.lists.models import UserList
|
||||
from apps.core.choices.serializers import RichChoiceFieldSerializer
|
||||
|
||||
UserModel = get_user_model()
|
||||
@@ -85,6 +85,8 @@ class UserProfileSerializer(serializers.ModelSerializer):
|
||||
"dark_ride_credits",
|
||||
"flat_ride_credits",
|
||||
"water_ride_credits",
|
||||
"unit_system",
|
||||
"location",
|
||||
]
|
||||
read_only_fields = ["profile_id", "avatar_url", "avatar_variants"]
|
||||
|
||||
@@ -503,8 +505,8 @@ class UserStatisticsSerializer(serializers.Serializer):
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Top List Example",
|
||||
summary="User's top list",
|
||||
"User List Example",
|
||||
summary="User's list",
|
||||
description="A user's ranked list of rides or parks",
|
||||
value={
|
||||
"id": 1,
|
||||
@@ -518,13 +520,13 @@ class UserStatisticsSerializer(serializers.Serializer):
|
||||
)
|
||||
]
|
||||
)
|
||||
class TopListSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for user's top lists."""
|
||||
class UserListSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for user's lists."""
|
||||
|
||||
items_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = TopList
|
||||
model = UserList
|
||||
fields = [
|
||||
"id",
|
||||
"title",
|
||||
@@ -611,6 +613,8 @@ class ProfileUpdateSerializer(serializers.ModelSerializer):
|
||||
"instagram",
|
||||
"youtube",
|
||||
"discord",
|
||||
"unit_system",
|
||||
"location",
|
||||
]
|
||||
|
||||
def validate_display_name(self, value):
|
||||
|
||||
@@ -73,6 +73,7 @@ urlpatterns = [
|
||||
path("email/", include("apps.api.v1.email.urls")),
|
||||
path("core/", include("apps.api.v1.core.urls")),
|
||||
path("maps/", include("apps.api.v1.maps.urls")),
|
||||
path("lists/", include("apps.lists.urls")),
|
||||
path("moderation/", include("apps.moderation.urls")),
|
||||
# Cloudflare Images Toolkit API endpoints
|
||||
path("cloudflare-images/", include("django_cloudflareimages_toolkit.urls")),
|
||||
|
||||
Reference in New Issue
Block a user