feat: Add blog, media, and support apps, implement ride credits and image API, and remove toplist feature.

This commit is contained in:
pacnpal
2025-12-26 15:15:28 -05:00
parent cd8868a591
commit 00699d53b4
77 changed files with 7274 additions and 538 deletions

View File

@@ -1,6 +1,6 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_field
from apps.accounts.models import UserProfile, TopList, TopListItem
from apps.accounts.models import UserProfile
from apps.accounts.serializers import UserSerializer # existing shared user serializer
@@ -11,10 +11,21 @@ class UserProfileCreateInputSerializer(serializers.ModelSerializer):
class UserProfileUpdateInputSerializer(serializers.ModelSerializer):
cloudflare_image_id = serializers.CharField(write_only=True, required=False)
class Meta:
model = UserProfile
fields = "__all__"
extra_kwargs = {"user": {"read_only": True}}
extra_kwargs = {"user": {"read_only": True}, "avatar": {"read_only": True}}
def update(self, instance, validated_data):
cloudflare_id = validated_data.pop("cloudflare_image_id", None)
if cloudflare_id:
from django_cloudflareimages_toolkit.models import CloudflareImage
image, _ = CloudflareImage.objects.get_or_create(cloudflare_id=cloudflare_id)
instance.avatar = image
return super().update(instance, validated_data)
class UserProfileOutputSerializer(serializers.ModelSerializer):
@@ -38,49 +49,3 @@ class UserProfileOutputSerializer(serializers.ModelSerializer):
if avatar:
return getattr(avatar, "url", None)
return None
class TopListItemCreateInputSerializer(serializers.ModelSerializer):
class Meta:
model = TopListItem
fields = "__all__"
class TopListItemUpdateInputSerializer(serializers.ModelSerializer):
class Meta:
model = TopListItem
fields = "__all__"
# allow updates, adjust as needed
extra_kwargs = {"top_list": {"read_only": False}}
class TopListItemOutputSerializer(serializers.ModelSerializer):
# Remove the ride field since it doesn't exist on the model
# The model likely uses a generic foreign key or different field name
class Meta:
model = TopListItem
fields = "__all__"
class TopListCreateInputSerializer(serializers.ModelSerializer):
class Meta:
model = TopList
fields = "__all__"
class TopListUpdateInputSerializer(serializers.ModelSerializer):
class Meta:
model = TopList
fields = "__all__"
# user is set by view's perform_create
extra_kwargs = {"user": {"read_only": True}}
class TopListOutputSerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True)
items = TopListItemOutputSerializer(many=True, read_only=True)
class Meta:
model = TopList
fields = "__all__"

View File

@@ -33,6 +33,8 @@ urlpatterns = [
views.cancel_account_deletion,
name="cancel_account_deletion",
),
# Data Export endpoint
path("data-export/", views.export_user_data, name="export_user_data"),
# User profile endpoints
path("profile/", views.get_user_profile, name="get_user_profile"),
path("profile/account/", views.update_user_account, name="update_user_account"),
@@ -106,4 +108,19 @@ urlpatterns = [
path("profile/avatar/upload/", views.upload_avatar, name="upload_avatar"),
path("profile/avatar/save/", views.save_avatar_image, name="save_avatar_image"),
path("profile/avatar/delete/", views.delete_avatar, name="delete_avatar"),
# Public Profile
path("profiles/<str:username>/", views.get_public_user_profile, name="get_public_user_profile"),
]
# Register ViewSets
from rest_framework.routers import DefaultRouter
from . import views_credits
from django.urls import include
router = DefaultRouter()
router.register(r"credits", views_credits.RideCreditViewSet, basename="ride-credit")
urlpatterns += [
path("", include(router.urls)),
]

View File

@@ -8,6 +8,7 @@ preferences, privacy, notifications, and security.
from apps.api.v1.serializers.accounts import (
CompleteUserSerializer,
PublicUserSerializer,
UserPreferencesSerializer,
NotificationSettingsSerializer,
PrivacySettingsSerializer,
@@ -23,6 +24,7 @@ from apps.api.v1.serializers.accounts import (
AvatarUploadSerializer,
)
from apps.accounts.services import UserDeletionService
from apps.accounts.export_service import UserExportService
from apps.accounts.models import (
User,
UserProfile,
@@ -1583,6 +1585,57 @@ def upload_avatar(request):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@extend_schema(
operation_id="export_user_data",
summary="Export all user data",
description="Generate a JSON dump of all user data including profile, reviews, and lists.",
responses={
200: {
"description": "User data export",
"example": {
"account": {"username": "user", "email": "user@example.com"},
"profile": {"display_name": "User"},
"content": {"park_reviews": [], "lists": []}
}
},
401: {"description": "Authentication required"},
},
tags=["Self-Service Account Management"],
)
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def export_user_data(request):
"""Export all user data as JSON."""
try:
export_data = UserExportService.export_user_data(request.user)
return Response(export_data, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error exporting data for user {request.user.id}: {e}", exc_info=True)
return Response(
{"error": "Failed to generate data export"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@extend_schema(
operation_id="get_public_user_profile",
summary="Get public user profile",
description="Get the public profile of a user by username.",
responses={
200: PublicUserSerializer,
404: {"description": "User not found"},
},
tags=["User Profile"],
)
@api_view(["GET"])
@permission_classes([AllowAny])
def get_public_user_profile(request, username):
"""Get public user profile by username."""
user = get_object_or_404(User, username=username)
serializer = PublicUserSerializer(user)
return Response(serializer.data, status=status.HTTP_200_OK)
# === MISSING FUNCTION IMPLEMENTATIONS ===

View File

@@ -0,0 +1,51 @@
from rest_framework import viewsets, permissions, filters
from django_filters.rest_framework import DjangoFilterBackend
from apps.rides.models.credits import RideCredit
from apps.api.v1.serializers.ride_credits import RideCreditSerializer
from drf_spectacular.utils import extend_schema, OpenApiParameter
from drf_spectacular.types import OpenApiTypes
class RideCreditViewSet(viewsets.ModelViewSet):
"""
ViewSet for managing Ride Credits.
Allows users to track rides they have ridden.
"""
serializer_class = RideCreditSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ['user__username', 'ride__park__slug', 'ride__manufacturer__slug']
ordering_fields = ['first_ridden_at', 'last_ridden_at', 'created_at', 'count', 'rating']
ordering = ['-last_ridden_at']
def get_queryset(self):
"""
Return ride credits.
Optionally filter by user via query param ?user=username
"""
queryset = RideCredit.objects.all().select_related('ride', 'ride__park', 'user')
# Filter by user if provided
username = self.request.query_params.get('user')
if username:
queryset = queryset.filter(user__username=username)
return queryset
def perform_create(self, serializer):
"""Associate the current user with the ride credit."""
serializer.save(user=self.request.user)
@extend_schema(
summary="List ride credits",
description="List ride credits. filter by user username.",
parameters=[
OpenApiParameter(
name="user",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR,
description="Filter by username",
),
]
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)