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

@@ -90,12 +90,7 @@ _ACCOUNTS_SYMBOLS: List[str] = [
"UserProfileOutputSerializer",
"UserProfileCreateInputSerializer",
"UserProfileUpdateInputSerializer",
"TopListOutputSerializer",
"TopListCreateInputSerializer",
"TopListUpdateInputSerializer",
"TopListItemOutputSerializer",
"TopListItemCreateInputSerializer",
"TopListItemUpdateInputSerializer",
"UserOutputSerializer",
"LoginInputSerializer",
"LoginOutputSerializer",

View File

@@ -18,6 +18,7 @@ from apps.accounts.models import (
NotificationPreference,
)
from apps.lists.models import UserList
from apps.rides.models.credits import RideCredit
from apps.core.choices.serializers import RichChoiceFieldSerializer
UserModel = get_user_model()
@@ -66,6 +67,8 @@ class UserProfileSerializer(serializers.ModelSerializer):
avatar_url = serializers.SerializerMethodField()
avatar_variants = serializers.SerializerMethodField()
total_credits = serializers.SerializerMethodField()
unique_parks = serializers.SerializerMethodField()
class Meta:
model = UserProfile
@@ -87,8 +90,19 @@ class UserProfileSerializer(serializers.ModelSerializer):
"water_ride_credits",
"unit_system",
"location",
"total_credits",
"unique_parks",
]
read_only_fields = ["profile_id", "avatar_url", "avatar_variants"]
read_only_fields = ["profile_id", "avatar_url", "avatar_variants", "total_credits", "unique_parks"]
def get_total_credits(self, obj):
"""Get the total number of ride credits."""
return RideCredit.objects.filter(user=obj.user).count()
def get_unique_parks(self, obj):
"""Get the number of unique parks visited."""
# This assumes RideCredit -> Ride -> Park relationship
return RideCredit.objects.filter(user=obj.user).values("ride__park").distinct().count()
def get_avatar_url(self, obj):
"""Get the avatar URL with fallback to default letter-based avatar."""
@@ -167,6 +181,25 @@ class CompleteUserSerializer(serializers.ModelSerializer):
read_only_fields = ["user_id", "date_joined", "role"]
class PublicUserSerializer(serializers.ModelSerializer):
"""
Public user serializer for viewing other users' profiles.
Only exposes public information.
"""
profile = UserProfileSerializer(read_only=True)
class Meta:
model = User
fields = [
"user_id",
"username",
"date_joined",
"role",
"profile",
]
read_only_fields = fields
# === USER SETTINGS SERIALIZERS ===

View File

@@ -0,0 +1,171 @@
"""
Serializers for park review API endpoints.
This module contains serializers for park review CRUD operations.
"""
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer, OpenApiExample
from apps.parks.models.reviews import ParkReview
from apps.api.v1.serializers.reviews import ReviewUserSerializer
@extend_schema_serializer(
examples=[
OpenApiExample(
name="Complete Park Review",
summary="Full park review response",
description="Example response showing all fields for a park review",
value={
"id": 123,
"title": "Great family park!",
"content": "We had a wonderful time. The atmosphere is charming.",
"rating": 9,
"visit_date": "2023-06-15",
"created_at": "2023-06-16T10:30:00Z",
"updated_at": "2023-06-16T10:30:00Z",
"is_published": True,
"user": {
"username": "park_fan",
"display_name": "Park Fan",
"avatar_url": "https://example.com/avatar.jpg"
},
"park": {
"id": 101,
"name": "Cedar Point",
"slug": "cedar-point"
}
}
)
]
)
class ParkReviewOutputSerializer(serializers.ModelSerializer):
"""Output serializer for park reviews."""
user = ReviewUserSerializer(read_only=True)
park = serializers.SerializerMethodField()
class Meta:
model = ParkReview
fields = [
"id",
"title",
"content",
"rating",
"visit_date",
"created_at",
"updated_at",
"is_published",
"user",
"park",
]
read_only_fields = [
"id",
"created_at",
"updated_at",
"user",
"park",
]
@extend_schema_field(serializers.DictField())
def get_park(self, obj):
"""Get park information."""
return {
"id": obj.park.id,
"name": obj.park.name,
"slug": obj.park.slug,
}
class ParkReviewCreateInputSerializer(serializers.ModelSerializer):
"""Input serializer for creating park reviews."""
class Meta:
model = ParkReview
fields = [
"title",
"content",
"rating",
"visit_date",
]
def validate_rating(self, value):
"""Validate rating is between 1 and 10."""
if not (1 <= value <= 10):
raise serializers.ValidationError("Rating must be between 1 and 10.")
return value
class ParkReviewUpdateInputSerializer(serializers.ModelSerializer):
"""Input serializer for updating park reviews."""
class Meta:
model = ParkReview
fields = [
"title",
"content",
"rating",
"visit_date",
]
def validate_rating(self, value):
"""Validate rating is between 1 and 10."""
if not (1 <= value <= 10):
raise serializers.ValidationError("Rating must be between 1 and 10.")
return value
class ParkReviewListOutputSerializer(serializers.ModelSerializer):
"""Simplified output serializer for park review lists."""
user = ReviewUserSerializer(read_only=True)
park_name = serializers.CharField(source="park.name", read_only=True)
class Meta:
model = ParkReview
fields = [
"id",
"title",
"rating",
"visit_date",
"created_at",
"is_published",
"user",
"park_name",
]
read_only_fields = fields
class ParkReviewStatsOutputSerializer(serializers.Serializer):
"""Output serializer for park review statistics."""
total_reviews = serializers.IntegerField()
published_reviews = serializers.IntegerField()
pending_reviews = serializers.IntegerField()
average_rating = serializers.FloatField(allow_null=True)
rating_distribution = serializers.DictField(
child=serializers.IntegerField(),
help_text="Count of reviews by rating (1-10)"
)
recent_reviews = serializers.IntegerField()
class ParkReviewModerationInputSerializer(serializers.Serializer):
"""Input serializer for review moderation operations."""
review_ids = serializers.ListField(
child=serializers.IntegerField(),
help_text="List of review IDs to moderate"
)
action = serializers.ChoiceField(
choices=[
("publish", "Publish"),
("unpublish", "Unpublish"),
("delete", "Delete"),
],
help_text="Moderation action to perform"
)
moderation_notes = serializers.CharField(
required=False,
allow_blank=True,
help_text="Optional notes about the moderation action"
)

View File

@@ -0,0 +1,47 @@
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_field
from apps.rides.models.credits import RideCredit
from apps.rides.models import Ride
from apps.api.v1.serializers.rides import RideListOutputSerializer
class RideCreditSerializer(serializers.ModelSerializer):
"""Serializer for user ride credits."""
ride_id = serializers.PrimaryKeyRelatedField(
queryset=Ride.objects.all(), source='ride', write_only=True
)
ride = RideListOutputSerializer(read_only=True)
class Meta:
model = RideCredit
fields = [
'id',
'ride',
'ride_id',
'count',
'rating',
'first_ridden_at',
'last_ridden_at',
'notes',
'created_at',
'updated_at',
]
read_only_fields = ['id', 'created_at', 'updated_at']
def validate(self, attrs):
"""
Validate data.
"""
# Ensure dates make sense
first = attrs.get('first_ridden_at')
last = attrs.get('last_ridden_at')
if first and last and last < first:
raise serializers.ValidationError("Last ridden date cannot be before first ridden date.")
return attrs
def create(self, validated_data):
"""Create a new ride credit."""
user = self.context['request'].user
validated_data['user'] = user
return super().create(validated_data)