mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 18:51:11 -05:00
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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user