Add ride credits and top lists endpoints for API v1

- Implement CRUD operations for ride credits, allowing users to log rides, track counts, and view statistics.
- Create endpoints for managing user-created ranked lists of parks, rides, or coasters with custom rankings and notes.
- Introduce pagination for both ride credits and top lists.
- Ensure proper authentication and authorization for modifying user-specific data.
- Add serialization methods for ride credits and top lists to return structured data.
- Include error handling and logging for better traceability of operations.
This commit is contained in:
pacnpal
2025-11-08 16:02:11 -05:00
parent 00985eac8d
commit e38a9aaa41
11 changed files with 2176 additions and 0 deletions

View File

@@ -967,3 +967,257 @@ class RideSearchFilters(BaseModel):
min_speed: Optional[Decimal] = Field(None, description="Minimum speed in mph")
max_speed: Optional[Decimal] = Field(None, description="Maximum speed in mph")
limit: int = Field(20, ge=1, le=100)
# ============================================================================
# Review Schemas
# ============================================================================
class UserSchema(BaseModel):
"""Minimal user schema for embedding in other schemas."""
id: UUID
username: str
display_name: str
avatar_url: Optional[str] = None
reputation_score: int
class Config:
from_attributes = True
class ReviewCreateSchema(BaseModel):
"""Schema for creating a review."""
entity_type: str = Field(..., description="Entity type: 'park' or 'ride'")
entity_id: UUID = Field(..., description="ID of park or ride being reviewed")
title: str = Field(..., min_length=1, max_length=200, description="Review title")
content: str = Field(..., min_length=10, description="Review content (min 10 characters)")
rating: int = Field(..., ge=1, le=5, description="Rating from 1 to 5 stars")
visit_date: Optional[date] = Field(None, description="Date of visit")
wait_time_minutes: Optional[int] = Field(None, ge=0, description="Wait time in minutes")
@field_validator('entity_type')
def validate_entity_type(cls, v):
if v not in ['park', 'ride']:
raise ValueError('entity_type must be "park" or "ride"')
return v
class ReviewUpdateSchema(BaseModel):
"""Schema for updating a review."""
title: Optional[str] = Field(None, min_length=1, max_length=200)
content: Optional[str] = Field(None, min_length=10)
rating: Optional[int] = Field(None, ge=1, le=5)
visit_date: Optional[date] = None
wait_time_minutes: Optional[int] = Field(None, ge=0)
class VoteRequest(BaseModel):
"""Schema for voting on a review."""
is_helpful: bool = Field(..., description="True if helpful, False if not helpful")
class ReviewOut(TimestampSchema):
"""Schema for review output."""
id: int
user: UserSchema
entity_type: str
entity_id: UUID
entity_name: str
title: str
content: str
rating: int
visit_date: Optional[date]
wait_time_minutes: Optional[int]
helpful_votes: int
total_votes: int
helpful_percentage: Optional[float]
moderation_status: str
moderated_at: Optional[datetime]
moderated_by_email: Optional[str]
photo_count: int
user_vote: Optional[bool] = None # Current user's vote if authenticated
class Config:
from_attributes = True
class VoteResponse(BaseModel):
"""Response for vote action."""
success: bool
review_id: int
helpful_votes: int
total_votes: int
helpful_percentage: Optional[float]
class ReviewListOut(BaseModel):
"""Paginated review list response."""
items: List[ReviewOut]
total: int
page: int
page_size: int
total_pages: int
class ReviewStatsOut(BaseModel):
"""Statistics about reviews for an entity."""
average_rating: float
total_reviews: int
rating_distribution: dict # {1: count, 2: count, 3: count, 4: count, 5: count}
# ============================================================================
# Ride Credit Schemas
# ============================================================================
class RideCreditCreateSchema(BaseModel):
"""Schema for creating a ride credit."""
ride_id: UUID = Field(..., description="ID of ride")
first_ride_date: Optional[date] = Field(None, description="Date of first ride")
ride_count: int = Field(1, ge=1, description="Number of times ridden")
notes: Optional[str] = Field(None, max_length=500, description="Notes about the ride")
class RideCreditUpdateSchema(BaseModel):
"""Schema for updating a ride credit."""
first_ride_date: Optional[date] = None
ride_count: Optional[int] = Field(None, ge=1)
notes: Optional[str] = Field(None, max_length=500)
class RideCreditOut(TimestampSchema):
"""Schema for ride credit output."""
id: UUID
user: UserSchema
ride_id: UUID
ride_name: str
ride_slug: str
park_id: UUID
park_name: str
park_slug: str
is_coaster: bool
first_ride_date: Optional[date]
ride_count: int
notes: str
class Config:
from_attributes = True
class RideCreditListOut(BaseModel):
"""Paginated ride credit list response."""
items: List[RideCreditOut]
total: int
page: int
page_size: int
total_pages: int
class RideCreditStatsOut(BaseModel):
"""Statistics about user's ride credits."""
total_rides: int # Sum of all ride_counts
total_credits: int # Count of unique rides
unique_parks: int
coaster_count: int
first_credit_date: Optional[date]
last_credit_date: Optional[date]
top_park: Optional[str]
top_park_count: int
recent_credits: List[RideCreditOut]
# ============================================================================
# Top List Schemas
# ============================================================================
class TopListCreateSchema(BaseModel):
"""Schema for creating a top list."""
list_type: str = Field(..., description="List type: 'parks', 'rides', or 'coasters'")
title: str = Field(..., min_length=1, max_length=200, description="List title")
description: Optional[str] = Field(None, max_length=1000, description="List description")
is_public: bool = Field(True, description="Whether list is publicly visible")
@field_validator('list_type')
def validate_list_type(cls, v):
if v not in ['parks', 'rides', 'coasters']:
raise ValueError('list_type must be "parks", "rides", or "coasters"')
return v
class TopListUpdateSchema(BaseModel):
"""Schema for updating a top list."""
title: Optional[str] = Field(None, min_length=1, max_length=200)
description: Optional[str] = Field(None, max_length=1000)
is_public: Optional[bool] = None
class TopListItemCreateSchema(BaseModel):
"""Schema for creating a top list item."""
entity_type: str = Field(..., description="Entity type: 'park' or 'ride'")
entity_id: UUID = Field(..., description="ID of park or ride")
position: Optional[int] = Field(None, ge=1, description="Position in list (1 = top)")
notes: Optional[str] = Field(None, max_length=500, description="Notes about this item")
@field_validator('entity_type')
def validate_entity_type(cls, v):
if v not in ['park', 'ride']:
raise ValueError('entity_type must be "park" or "ride"')
return v
class TopListItemUpdateSchema(BaseModel):
"""Schema for updating a top list item."""
position: Optional[int] = Field(None, ge=1, description="New position in list")
notes: Optional[str] = Field(None, max_length=500)
class TopListItemOut(TimestampSchema):
"""Schema for top list item output."""
id: UUID
position: int
entity_type: str
entity_id: UUID
entity_name: str
entity_slug: str
entity_image_url: Optional[str]
park_name: Optional[str] # For rides, show which park
notes: str
class Config:
from_attributes = True
class TopListOut(TimestampSchema):
"""Schema for top list output."""
id: UUID
user: UserSchema
list_type: str
title: str
description: str
is_public: bool
item_count: int
class Config:
from_attributes = True
class TopListDetailOut(TopListOut):
"""Detailed top list with items."""
items: List[TopListItemOut]
class Config:
from_attributes = True
class TopListListOut(BaseModel):
"""Paginated top list response."""
items: List[TopListOut]
total: int
page: int
page_size: int
total_pages: int
class ReorderItemsRequest(BaseModel):
"""Schema for reordering list items."""
item_positions: dict = Field(..., description="Map of item_id to new_position")