mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 04:31:16 -05:00
1566 lines
49 KiB
Python
1566 lines
49 KiB
Python
"""
|
|
Pydantic schemas for API v1 endpoints.
|
|
|
|
These schemas define the structure of request and response data for the REST API.
|
|
"""
|
|
from datetime import date, datetime
|
|
from typing import Optional, List
|
|
from decimal import Decimal
|
|
from pydantic import BaseModel, Field, field_validator
|
|
from uuid import UUID
|
|
|
|
|
|
# ============================================================================
|
|
# Base Schemas
|
|
# ============================================================================
|
|
|
|
class TimestampSchema(BaseModel):
|
|
"""Base schema with timestamps."""
|
|
created: datetime
|
|
modified: datetime
|
|
|
|
|
|
# ============================================================================
|
|
# Company Schemas
|
|
# ============================================================================
|
|
|
|
class CompanyBase(BaseModel):
|
|
"""Base company schema with common fields."""
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
description: Optional[str] = None
|
|
company_types: List[str] = Field(default_factory=list)
|
|
founded_date: Optional[date] = None
|
|
founded_date_precision: str = Field(default='day')
|
|
closed_date: Optional[date] = None
|
|
closed_date_precision: str = Field(default='day')
|
|
website: Optional[str] = None
|
|
logo_image_id: Optional[str] = None
|
|
logo_image_url: Optional[str] = None
|
|
|
|
|
|
class CompanyCreate(CompanyBase):
|
|
"""Schema for creating a company."""
|
|
pass
|
|
|
|
|
|
class CompanyUpdate(BaseModel):
|
|
"""Schema for updating a company (all fields optional)."""
|
|
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
description: Optional[str] = None
|
|
company_types: Optional[List[str]] = None
|
|
founded_date: Optional[date] = None
|
|
founded_date_precision: Optional[str] = None
|
|
closed_date: Optional[date] = None
|
|
closed_date_precision: Optional[str] = None
|
|
website: Optional[str] = None
|
|
logo_image_id: Optional[str] = None
|
|
logo_image_url: Optional[str] = None
|
|
|
|
|
|
class CompanyOut(CompanyBase, TimestampSchema):
|
|
"""Schema for company output."""
|
|
id: UUID
|
|
slug: str
|
|
park_count: int
|
|
ride_count: int
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# RideModel Schemas
|
|
# ============================================================================
|
|
|
|
class RideModelBase(BaseModel):
|
|
"""Base ride model schema."""
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
description: Optional[str] = None
|
|
manufacturer_id: UUID
|
|
model_type: str
|
|
typical_height: Optional[Decimal] = None
|
|
typical_speed: Optional[Decimal] = None
|
|
typical_capacity: Optional[int] = None
|
|
image_id: Optional[str] = None
|
|
image_url: Optional[str] = None
|
|
|
|
|
|
class RideModelCreate(RideModelBase):
|
|
"""Schema for creating a ride model."""
|
|
pass
|
|
|
|
|
|
class RideModelUpdate(BaseModel):
|
|
"""Schema for updating a ride model (all fields optional)."""
|
|
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
description: Optional[str] = None
|
|
manufacturer_id: Optional[UUID] = None
|
|
model_type: Optional[str] = None
|
|
typical_height: Optional[Decimal] = None
|
|
typical_speed: Optional[Decimal] = None
|
|
typical_capacity: Optional[int] = None
|
|
image_id: Optional[str] = None
|
|
image_url: Optional[str] = None
|
|
|
|
|
|
class RideModelOut(RideModelBase, TimestampSchema):
|
|
"""Schema for ride model output."""
|
|
id: UUID
|
|
slug: str
|
|
installation_count: int
|
|
manufacturer_name: Optional[str] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# Park Schemas
|
|
# ============================================================================
|
|
|
|
class ParkBase(BaseModel):
|
|
"""Base park schema."""
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
description: Optional[str] = None
|
|
park_type: str
|
|
status: str = Field(default='operating')
|
|
opening_date: Optional[date] = None
|
|
opening_date_precision: str = Field(default='day')
|
|
closing_date: Optional[date] = None
|
|
closing_date_precision: str = Field(default='day')
|
|
latitude: Optional[Decimal] = None
|
|
longitude: Optional[Decimal] = None
|
|
operator_id: Optional[UUID] = None
|
|
website: Optional[str] = None
|
|
banner_image_id: Optional[str] = None
|
|
banner_image_url: Optional[str] = None
|
|
logo_image_id: Optional[str] = None
|
|
logo_image_url: Optional[str] = None
|
|
|
|
|
|
class ParkCreate(ParkBase):
|
|
"""Schema for creating a park."""
|
|
pass
|
|
|
|
|
|
class ParkUpdate(BaseModel):
|
|
"""Schema for updating a park (all fields optional)."""
|
|
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
description: Optional[str] = None
|
|
park_type: Optional[str] = None
|
|
status: Optional[str] = None
|
|
opening_date: Optional[date] = None
|
|
opening_date_precision: Optional[str] = None
|
|
closing_date: Optional[date] = None
|
|
closing_date_precision: Optional[str] = None
|
|
latitude: Optional[Decimal] = None
|
|
longitude: Optional[Decimal] = None
|
|
operator_id: Optional[UUID] = None
|
|
website: Optional[str] = None
|
|
banner_image_id: Optional[str] = None
|
|
banner_image_url: Optional[str] = None
|
|
logo_image_id: Optional[str] = None
|
|
logo_image_url: Optional[str] = None
|
|
|
|
|
|
class ParkOut(ParkBase, TimestampSchema):
|
|
"""Schema for park output."""
|
|
id: UUID
|
|
slug: str
|
|
ride_count: int
|
|
coaster_count: int
|
|
operator_name: Optional[str] = None
|
|
coordinates: Optional[tuple[float, float]] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# Ride Schemas
|
|
# ============================================================================
|
|
|
|
class RideBase(BaseModel):
|
|
"""Base ride schema."""
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
description: Optional[str] = None
|
|
park_id: UUID
|
|
ride_category: str
|
|
ride_type: Optional[str] = None
|
|
is_coaster: bool = Field(default=False)
|
|
status: str = Field(default='operating')
|
|
opening_date: Optional[date] = None
|
|
opening_date_precision: str = Field(default='day')
|
|
closing_date: Optional[date] = None
|
|
closing_date_precision: str = Field(default='day')
|
|
manufacturer_id: Optional[UUID] = None
|
|
model_id: Optional[UUID] = None
|
|
height: Optional[Decimal] = None
|
|
speed: Optional[Decimal] = None
|
|
length: Optional[Decimal] = None
|
|
duration: Optional[int] = None
|
|
inversions: Optional[int] = None
|
|
capacity: Optional[int] = None
|
|
image_id: Optional[str] = None
|
|
image_url: Optional[str] = None
|
|
|
|
|
|
class RideCreate(RideBase):
|
|
"""Schema for creating a ride."""
|
|
pass
|
|
|
|
|
|
class RideUpdate(BaseModel):
|
|
"""Schema for updating a ride (all fields optional)."""
|
|
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
description: Optional[str] = None
|
|
park_id: Optional[UUID] = None
|
|
ride_category: Optional[str] = None
|
|
ride_type: Optional[str] = None
|
|
is_coaster: Optional[bool] = None
|
|
status: Optional[str] = None
|
|
opening_date: Optional[date] = None
|
|
opening_date_precision: Optional[str] = None
|
|
closing_date: Optional[date] = None
|
|
closing_date_precision: Optional[str] = None
|
|
manufacturer_id: Optional[UUID] = None
|
|
model_id: Optional[UUID] = None
|
|
height: Optional[Decimal] = None
|
|
speed: Optional[Decimal] = None
|
|
length: Optional[Decimal] = None
|
|
duration: Optional[int] = None
|
|
inversions: Optional[int] = None
|
|
capacity: Optional[int] = None
|
|
image_id: Optional[str] = None
|
|
image_url: Optional[str] = None
|
|
|
|
|
|
class RideOut(RideBase, TimestampSchema):
|
|
"""Schema for ride output."""
|
|
id: UUID
|
|
slug: str
|
|
park_name: Optional[str] = None
|
|
manufacturer_name: Optional[str] = None
|
|
model_name: Optional[str] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# Ride Name History Schemas
|
|
# ============================================================================
|
|
|
|
class RideNameHistoryOut(BaseModel):
|
|
"""Schema for ride name history output."""
|
|
id: UUID
|
|
former_name: str
|
|
from_year: Optional[int] = None
|
|
to_year: Optional[int] = None
|
|
date_changed: Optional[date] = None
|
|
date_changed_precision: Optional[str] = None
|
|
reason: Optional[str] = None
|
|
order_index: Optional[int] = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# ============================================================================
|
|
# Pagination Schemas
|
|
# ============================================================================
|
|
|
|
class PaginatedResponse(BaseModel):
|
|
"""Generic paginated response schema."""
|
|
items: List
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
total_pages: int
|
|
|
|
|
|
class CompanyListOut(BaseModel):
|
|
"""Paginated company list response."""
|
|
items: List[CompanyOut]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
total_pages: int
|
|
|
|
|
|
class RideModelListOut(BaseModel):
|
|
"""Paginated ride model list response."""
|
|
items: List[RideModelOut]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
total_pages: int
|
|
|
|
|
|
class ParkListOut(BaseModel):
|
|
"""Paginated park list response."""
|
|
items: List[ParkOut]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
total_pages: int
|
|
|
|
|
|
class RideListOut(BaseModel):
|
|
"""Paginated ride list response."""
|
|
items: List[RideOut]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
total_pages: int
|
|
|
|
|
|
# ============================================================================
|
|
# Error Schemas
|
|
# ============================================================================
|
|
|
|
class ErrorResponse(BaseModel):
|
|
"""Standard error response schema."""
|
|
detail: str
|
|
code: Optional[str] = None
|
|
|
|
|
|
class ValidationErrorResponse(BaseModel):
|
|
"""Validation error response schema."""
|
|
detail: str
|
|
errors: Optional[List[dict]] = None
|
|
|
|
|
|
# ============================================================================
|
|
# Moderation Schemas
|
|
# ============================================================================
|
|
|
|
class SubmissionItemBase(BaseModel):
|
|
"""Base submission item schema."""
|
|
field_name: str = Field(..., min_length=1, max_length=100)
|
|
field_label: Optional[str] = None
|
|
old_value: Optional[dict] = None
|
|
new_value: Optional[dict] = None
|
|
change_type: str = Field(default='modify')
|
|
is_required: bool = Field(default=False)
|
|
order: int = Field(default=0)
|
|
|
|
|
|
class SubmissionItemCreate(SubmissionItemBase):
|
|
"""Schema for creating a submission item."""
|
|
pass
|
|
|
|
|
|
class SubmissionItemOut(SubmissionItemBase, TimestampSchema):
|
|
"""Schema for submission item output."""
|
|
id: UUID
|
|
submission_id: UUID
|
|
status: str
|
|
reviewed_by_id: Optional[UUID] = None
|
|
reviewed_by_email: Optional[str] = None
|
|
reviewed_at: Optional[datetime] = None
|
|
rejection_reason: Optional[str] = None
|
|
old_value_display: str
|
|
new_value_display: str
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class ContentSubmissionBase(BaseModel):
|
|
"""Base content submission schema."""
|
|
submission_type: str
|
|
title: str = Field(..., min_length=1, max_length=255)
|
|
description: Optional[str] = None
|
|
entity_type: str
|
|
entity_id: UUID
|
|
|
|
|
|
class ContentSubmissionCreate(BaseModel):
|
|
"""Schema for creating a content submission."""
|
|
entity_type: str = Field(..., description="Entity type (park, ride, company, ridemodel)")
|
|
entity_id: UUID = Field(..., description="ID of entity being modified")
|
|
submission_type: str = Field(..., description="Operation type (create, update, delete)")
|
|
title: str = Field(..., min_length=1, max_length=255, description="Brief description")
|
|
description: Optional[str] = Field(None, description="Detailed description")
|
|
items: List[SubmissionItemCreate] = Field(..., min_items=1, description="List of changes")
|
|
metadata: Optional[dict] = Field(default_factory=dict)
|
|
auto_submit: bool = Field(default=True, description="Auto-submit for review")
|
|
|
|
|
|
class ContentSubmissionOut(TimestampSchema):
|
|
"""Schema for content submission output."""
|
|
id: UUID
|
|
status: str
|
|
submission_type: str
|
|
title: str
|
|
description: Optional[str] = None
|
|
entity_type: str
|
|
entity_id: UUID
|
|
user_id: UUID
|
|
user_email: str
|
|
locked_by_id: Optional[UUID] = None
|
|
locked_by_email: Optional[str] = None
|
|
locked_at: Optional[datetime] = None
|
|
reviewed_by_id: Optional[UUID] = None
|
|
reviewed_by_email: Optional[str] = None
|
|
reviewed_at: Optional[datetime] = None
|
|
rejection_reason: Optional[str] = None
|
|
source: str
|
|
metadata: dict
|
|
items_count: int
|
|
approved_items_count: int
|
|
rejected_items_count: int
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class ContentSubmissionDetail(ContentSubmissionOut):
|
|
"""Detailed submission with items."""
|
|
items: List[SubmissionItemOut]
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class StartReviewRequest(BaseModel):
|
|
"""Schema for starting a review."""
|
|
pass # No additional fields needed
|
|
|
|
|
|
class ApproveRequest(BaseModel):
|
|
"""Schema for approving a submission."""
|
|
pass # No additional fields needed
|
|
|
|
|
|
class ApproveSelectiveRequest(BaseModel):
|
|
"""Schema for selective approval."""
|
|
item_ids: List[UUID] = Field(..., min_items=1, description="List of item IDs to approve")
|
|
|
|
|
|
class RejectRequest(BaseModel):
|
|
"""Schema for rejecting a submission."""
|
|
reason: str = Field(..., min_length=1, description="Reason for rejection")
|
|
|
|
|
|
class RejectSelectiveRequest(BaseModel):
|
|
"""Schema for selective rejection."""
|
|
item_ids: List[UUID] = Field(..., min_items=1, description="List of item IDs to reject")
|
|
reason: Optional[str] = Field(None, description="Reason for rejection")
|
|
|
|
|
|
class ApprovalResponse(BaseModel):
|
|
"""Response for approval operations."""
|
|
success: bool
|
|
message: str
|
|
submission: ContentSubmissionOut
|
|
|
|
|
|
class SelectiveApprovalResponse(BaseModel):
|
|
"""Response for selective approval."""
|
|
success: bool
|
|
message: str
|
|
approved: int
|
|
total: int
|
|
pending: int
|
|
submission_approved: bool
|
|
|
|
|
|
class SelectiveRejectionResponse(BaseModel):
|
|
"""Response for selective rejection."""
|
|
success: bool
|
|
message: str
|
|
rejected: int
|
|
total: int
|
|
pending: int
|
|
submission_complete: bool
|
|
|
|
|
|
class SubmissionListOut(BaseModel):
|
|
"""Paginated submission list response."""
|
|
items: List[ContentSubmissionOut]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
total_pages: int
|
|
|
|
|
|
# ============================================================================
|
|
# Generic Utility Schemas
|
|
# ============================================================================
|
|
|
|
class MessageSchema(BaseModel):
|
|
"""Generic message response."""
|
|
message: str
|
|
success: bool = True
|
|
|
|
|
|
class ErrorSchema(BaseModel):
|
|
"""Standard error response."""
|
|
error: str
|
|
detail: Optional[str] = None
|
|
|
|
|
|
# ============================================================================
|
|
# Authentication Schemas
|
|
# ============================================================================
|
|
|
|
class UserBase(BaseModel):
|
|
"""Base user schema."""
|
|
email: str = Field(..., description="Email address")
|
|
username: Optional[str] = Field(None, description="Username")
|
|
first_name: Optional[str] = Field(None, max_length=150)
|
|
last_name: Optional[str] = Field(None, max_length=150)
|
|
|
|
|
|
class UserRegisterRequest(BaseModel):
|
|
"""Schema for user registration."""
|
|
email: str = Field(..., description="Email address")
|
|
password: str = Field(..., min_length=8, description="Password (min 8 characters)")
|
|
password_confirm: str = Field(..., description="Password confirmation")
|
|
username: Optional[str] = Field(None, description="Username (auto-generated if not provided)")
|
|
first_name: Optional[str] = Field(None, max_length=150)
|
|
last_name: Optional[str] = Field(None, max_length=150)
|
|
|
|
@field_validator('password_confirm')
|
|
def passwords_match(cls, v, info):
|
|
if 'password' in info.data and v != info.data['password']:
|
|
raise ValueError('Passwords do not match')
|
|
return v
|
|
|
|
|
|
class UserLoginRequest(BaseModel):
|
|
"""Schema for user login."""
|
|
email: str = Field(..., description="Email address")
|
|
password: str = Field(..., description="Password")
|
|
mfa_token: Optional[str] = Field(None, description="MFA token if enabled")
|
|
|
|
|
|
class TokenResponse(BaseModel):
|
|
"""Schema for token response."""
|
|
access: str = Field(..., description="JWT access token")
|
|
refresh: str = Field(..., description="JWT refresh token")
|
|
token_type: str = Field(default="Bearer")
|
|
|
|
|
|
class TokenRefreshRequest(BaseModel):
|
|
"""Schema for token refresh."""
|
|
refresh: str = Field(..., description="Refresh token")
|
|
|
|
|
|
class UserProfileOut(BaseModel):
|
|
"""Schema for user profile output."""
|
|
id: UUID
|
|
email: str
|
|
username: str
|
|
first_name: str
|
|
last_name: str
|
|
display_name: str
|
|
avatar_url: Optional[str] = None
|
|
bio: Optional[str] = None
|
|
reputation_score: int
|
|
mfa_enabled: bool
|
|
banned: bool
|
|
date_joined: datetime
|
|
last_login: Optional[datetime] = None
|
|
oauth_provider: str
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class UserProfileUpdate(BaseModel):
|
|
"""Schema for updating user profile."""
|
|
first_name: Optional[str] = Field(None, max_length=150)
|
|
last_name: Optional[str] = Field(None, max_length=150)
|
|
username: Optional[str] = Field(None, max_length=150)
|
|
bio: Optional[str] = Field(None, max_length=500)
|
|
avatar_url: Optional[str] = None
|
|
|
|
|
|
class ChangePasswordRequest(BaseModel):
|
|
"""Schema for password change."""
|
|
old_password: str = Field(..., description="Current password")
|
|
new_password: str = Field(..., min_length=8, description="New password")
|
|
new_password_confirm: str = Field(..., description="New password confirmation")
|
|
|
|
@field_validator('new_password_confirm')
|
|
def passwords_match(cls, v, info):
|
|
if 'new_password' in info.data and v != info.data['new_password']:
|
|
raise ValueError('Passwords do not match')
|
|
return v
|
|
|
|
|
|
class ResetPasswordRequest(BaseModel):
|
|
"""Schema for password reset."""
|
|
email: str = Field(..., description="Email address")
|
|
|
|
|
|
class ResetPasswordConfirm(BaseModel):
|
|
"""Schema for password reset confirmation."""
|
|
token: str = Field(..., description="Reset token")
|
|
password: str = Field(..., min_length=8, description="New password")
|
|
password_confirm: str = Field(..., description="Password confirmation")
|
|
|
|
@field_validator('password_confirm')
|
|
def passwords_match(cls, v, info):
|
|
if 'password' in info.data and v != info.data['password']:
|
|
raise ValueError('Passwords do not match')
|
|
return v
|
|
|
|
|
|
class UserRoleOut(BaseModel):
|
|
"""Schema for user role output."""
|
|
role: str
|
|
is_moderator: bool
|
|
is_admin: bool
|
|
granted_at: datetime
|
|
granted_by_email: Optional[str] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class UserPermissionsOut(BaseModel):
|
|
"""Schema for user permissions."""
|
|
can_submit: bool
|
|
can_moderate: bool
|
|
can_admin: bool
|
|
can_edit_own: bool
|
|
can_delete_own: bool
|
|
|
|
|
|
class UserStatsOut(BaseModel):
|
|
"""Schema for user statistics."""
|
|
total_submissions: int
|
|
approved_submissions: int
|
|
reputation_score: int
|
|
member_since: datetime
|
|
last_active: Optional[datetime] = None
|
|
|
|
|
|
class UserProfilePreferencesOut(BaseModel):
|
|
"""Schema for user preferences."""
|
|
email_notifications: bool
|
|
email_on_submission_approved: bool
|
|
email_on_submission_rejected: bool
|
|
profile_public: bool
|
|
show_email: bool
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class UserProfilePreferencesUpdate(BaseModel):
|
|
"""Schema for updating user preferences."""
|
|
email_notifications: Optional[bool] = None
|
|
email_on_submission_approved: Optional[bool] = None
|
|
email_on_submission_rejected: Optional[bool] = None
|
|
profile_public: Optional[bool] = None
|
|
show_email: Optional[bool] = None
|
|
|
|
|
|
class TOTPEnableResponse(BaseModel):
|
|
"""Schema for TOTP enable response."""
|
|
secret: str = Field(..., description="TOTP secret key")
|
|
qr_code_url: str = Field(..., description="QR code URL for authenticator apps")
|
|
backup_codes: List[str] = Field(default_factory=list, description="Backup codes")
|
|
|
|
|
|
class TOTPConfirmRequest(BaseModel):
|
|
"""Schema for TOTP confirmation."""
|
|
token: str = Field(..., min_length=6, max_length=6, description="6-digit TOTP token")
|
|
|
|
|
|
class TOTPVerifyRequest(BaseModel):
|
|
"""Schema for TOTP verification."""
|
|
token: str = Field(..., min_length=6, max_length=6, description="6-digit TOTP token")
|
|
|
|
|
|
class BanUserRequest(BaseModel):
|
|
"""Schema for banning a user."""
|
|
user_id: UUID = Field(..., description="User ID to ban")
|
|
reason: str = Field(..., min_length=1, description="Reason for ban")
|
|
|
|
|
|
class UnbanUserRequest(BaseModel):
|
|
"""Schema for unbanning a user."""
|
|
user_id: UUID = Field(..., description="User ID to unban")
|
|
|
|
|
|
class AssignRoleRequest(BaseModel):
|
|
"""Schema for assigning a role."""
|
|
user_id: UUID = Field(..., description="User ID")
|
|
role: str = Field(..., description="Role to assign (user, moderator, admin)")
|
|
|
|
|
|
class UserListOut(BaseModel):
|
|
"""Paginated user list response."""
|
|
items: List[UserProfileOut]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
total_pages: int
|
|
|
|
|
|
# ============================================================================
|
|
# Photo/Media Schemas
|
|
# ============================================================================
|
|
|
|
class PhotoBase(BaseModel):
|
|
"""Base photo schema."""
|
|
title: Optional[str] = Field(None, max_length=255)
|
|
description: Optional[str] = None
|
|
credit: Optional[str] = Field(None, max_length=255, description="Photo credit/attribution")
|
|
photo_type: str = Field(default='gallery', description="Type: main, gallery, banner, logo, thumbnail, other")
|
|
is_visible: bool = Field(default=True)
|
|
|
|
|
|
class PhotoUploadRequest(PhotoBase):
|
|
"""Schema for photo upload request (form data)."""
|
|
entity_type: Optional[str] = Field(None, description="Entity type to attach to")
|
|
entity_id: Optional[UUID] = Field(None, description="Entity ID to attach to")
|
|
|
|
|
|
class PhotoUpdate(BaseModel):
|
|
"""Schema for updating photo metadata."""
|
|
title: Optional[str] = Field(None, max_length=255)
|
|
description: Optional[str] = None
|
|
credit: Optional[str] = Field(None, max_length=255)
|
|
photo_type: Optional[str] = None
|
|
is_visible: Optional[bool] = None
|
|
display_order: Optional[int] = None
|
|
|
|
|
|
class PhotoOut(PhotoBase, TimestampSchema):
|
|
"""Schema for photo output."""
|
|
id: UUID
|
|
cloudflare_image_id: str
|
|
cloudflare_url: str
|
|
uploaded_by_id: UUID
|
|
uploaded_by_email: Optional[str] = None
|
|
moderation_status: str
|
|
moderated_by_id: Optional[UUID] = None
|
|
moderated_by_email: Optional[str] = None
|
|
moderated_at: Optional[datetime] = None
|
|
moderation_notes: Optional[str] = None
|
|
entity_type: Optional[str] = None
|
|
entity_id: Optional[str] = None
|
|
entity_name: Optional[str] = None
|
|
width: int
|
|
height: int
|
|
file_size: int
|
|
mime_type: str
|
|
display_order: int
|
|
|
|
# Generated URLs for different variants
|
|
thumbnail_url: Optional[str] = None
|
|
banner_url: Optional[str] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class PhotoListOut(BaseModel):
|
|
"""Paginated photo list response."""
|
|
items: List[PhotoOut]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
total_pages: int
|
|
|
|
|
|
class PhotoUploadResponse(BaseModel):
|
|
"""Response for photo upload."""
|
|
success: bool
|
|
message: str
|
|
photo: PhotoOut
|
|
|
|
|
|
class PhotoModerateRequest(BaseModel):
|
|
"""Schema for moderating a photo."""
|
|
status: str = Field(..., description="Status: approved, rejected, flagged")
|
|
notes: Optional[str] = Field(None, description="Moderation notes")
|
|
|
|
|
|
class PhotoReorderRequest(BaseModel):
|
|
"""Schema for reordering photos."""
|
|
photo_ids: List[int] = Field(..., min_items=1, description="Ordered list of photo IDs")
|
|
photo_type: Optional[str] = Field(None, description="Optional photo type filter")
|
|
|
|
|
|
class PhotoAttachRequest(BaseModel):
|
|
"""Schema for attaching photo to entity."""
|
|
photo_id: UUID = Field(..., description="Photo ID to attach")
|
|
photo_type: Optional[str] = Field('gallery', description="Photo type")
|
|
|
|
|
|
class PhotoStatsOut(BaseModel):
|
|
"""Statistics about photos."""
|
|
total_photos: int
|
|
pending_photos: int
|
|
approved_photos: int
|
|
rejected_photos: int
|
|
flagged_photos: int
|
|
total_size_mb: float
|
|
|
|
|
|
# ============================================================================
|
|
# Search Schemas
|
|
# ============================================================================
|
|
|
|
class SearchResultBase(BaseModel):
|
|
"""Base schema for search results."""
|
|
id: UUID
|
|
name: str
|
|
slug: str
|
|
entity_type: str
|
|
description: Optional[str] = None
|
|
image_url: Optional[str] = None
|
|
|
|
|
|
class CompanySearchResult(SearchResultBase):
|
|
"""Company search result."""
|
|
company_types: List[str] = Field(default_factory=list)
|
|
park_count: int = 0
|
|
ride_count: int = 0
|
|
|
|
|
|
class RideModelSearchResult(SearchResultBase):
|
|
"""Ride model search result."""
|
|
manufacturer_name: str
|
|
model_type: str
|
|
installation_count: int = 0
|
|
|
|
|
|
class ParkSearchResult(SearchResultBase):
|
|
"""Park search result."""
|
|
park_type: str
|
|
status: str
|
|
operator_name: Optional[str] = None
|
|
ride_count: int = 0
|
|
coaster_count: int = 0
|
|
coordinates: Optional[tuple[float, float]] = None
|
|
|
|
|
|
class RideSearchResult(SearchResultBase):
|
|
"""Ride search result."""
|
|
park_name: str
|
|
park_slug: str
|
|
manufacturer_name: Optional[str] = None
|
|
ride_category: str
|
|
status: str
|
|
is_coaster: bool
|
|
|
|
|
|
class GlobalSearchResponse(BaseModel):
|
|
"""Response for global search across all entities."""
|
|
query: str
|
|
total_results: int
|
|
companies: List[CompanySearchResult] = Field(default_factory=list)
|
|
ride_models: List[RideModelSearchResult] = Field(default_factory=list)
|
|
parks: List[ParkSearchResult] = Field(default_factory=list)
|
|
rides: List[RideSearchResult] = Field(default_factory=list)
|
|
|
|
|
|
class AutocompleteItem(BaseModel):
|
|
"""Single autocomplete suggestion."""
|
|
id: UUID
|
|
name: str
|
|
slug: str
|
|
entity_type: str
|
|
park_name: Optional[str] = None # For rides
|
|
manufacturer_name: Optional[str] = None # For ride models
|
|
|
|
|
|
class AutocompleteResponse(BaseModel):
|
|
"""Response for autocomplete suggestions."""
|
|
query: str
|
|
suggestions: List[AutocompleteItem]
|
|
|
|
|
|
class SearchFilters(BaseModel):
|
|
"""Base filters for search operations."""
|
|
q: str = Field(..., min_length=2, max_length=200, description="Search query")
|
|
entity_types: Optional[List[str]] = Field(None, description="Filter by entity types")
|
|
limit: int = Field(20, ge=1, le=100, description="Maximum results per entity type")
|
|
|
|
|
|
class CompanySearchFilters(BaseModel):
|
|
"""Filters for company search."""
|
|
q: str = Field(..., min_length=2, max_length=200, description="Search query")
|
|
company_types: Optional[List[str]] = Field(None, description="Filter by company types")
|
|
founded_after: Optional[date] = Field(None, description="Founded after date")
|
|
founded_before: Optional[date] = Field(None, description="Founded before date")
|
|
limit: int = Field(20, ge=1, le=100)
|
|
|
|
|
|
class RideModelSearchFilters(BaseModel):
|
|
"""Filters for ride model search."""
|
|
q: str = Field(..., min_length=2, max_length=200, description="Search query")
|
|
manufacturer_id: Optional[UUID] = Field(None, description="Filter by manufacturer")
|
|
model_type: Optional[str] = Field(None, description="Filter by model type")
|
|
limit: int = Field(20, ge=1, le=100)
|
|
|
|
|
|
class ParkSearchFilters(BaseModel):
|
|
"""Filters for park search."""
|
|
q: str = Field(..., min_length=2, max_length=200, description="Search query")
|
|
status: Optional[str] = Field(None, description="Filter by status")
|
|
park_type: Optional[str] = Field(None, description="Filter by park type")
|
|
operator_id: Optional[UUID] = Field(None, description="Filter by operator")
|
|
opening_after: Optional[date] = Field(None, description="Opened after date")
|
|
opening_before: Optional[date] = Field(None, description="Opened before date")
|
|
latitude: Optional[float] = Field(None, description="Search center latitude")
|
|
longitude: Optional[float] = Field(None, description="Search center longitude")
|
|
radius: Optional[float] = Field(None, ge=0, le=500, description="Search radius in km")
|
|
limit: int = Field(20, ge=1, le=100)
|
|
|
|
|
|
class RideSearchFilters(BaseModel):
|
|
"""Filters for ride search."""
|
|
q: str = Field(..., min_length=2, max_length=200, description="Search query")
|
|
park_id: Optional[UUID] = Field(None, description="Filter by park")
|
|
manufacturer_id: Optional[UUID] = Field(None, description="Filter by manufacturer")
|
|
model_id: Optional[UUID] = Field(None, description="Filter by model")
|
|
status: Optional[str] = Field(None, description="Filter by status")
|
|
ride_category: Optional[str] = Field(None, description="Filter by category")
|
|
is_coaster: Optional[bool] = Field(None, description="Filter coasters only")
|
|
opening_after: Optional[date] = Field(None, description="Opened after date")
|
|
opening_before: Optional[date] = Field(None, description="Opened before date")
|
|
min_height: Optional[Decimal] = Field(None, description="Minimum height in feet")
|
|
max_height: Optional[Decimal] = Field(None, description="Maximum height in feet")
|
|
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")
|
|
|
|
|
|
# ============================================================================
|
|
# History/Versioning Schemas
|
|
# ============================================================================
|
|
|
|
class HistoryEventSchema(BaseModel):
|
|
"""Schema for a single history event."""
|
|
id: int
|
|
timestamp: datetime
|
|
operation: str # 'INSERT' or 'UPDATE'
|
|
snapshot: dict
|
|
changed_fields: Optional[dict] = None
|
|
change_summary: str
|
|
can_rollback: bool
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class HistoryListResponse(BaseModel):
|
|
"""Response for list history endpoint."""
|
|
entity_id: UUID
|
|
entity_type: str
|
|
entity_name: str
|
|
total_events: int
|
|
accessible_events: int
|
|
access_limited: bool
|
|
access_reason: str
|
|
events: List[HistoryEventSchema]
|
|
pagination: dict
|
|
|
|
|
|
class HistoryEventDetailSchema(BaseModel):
|
|
"""Detailed event with rollback preview."""
|
|
id: int
|
|
timestamp: datetime
|
|
operation: str
|
|
entity_id: UUID
|
|
entity_type: str
|
|
entity_name: str
|
|
snapshot: dict
|
|
changed_fields: Optional[dict] = None
|
|
metadata: dict
|
|
can_rollback: bool
|
|
rollback_preview: Optional[dict] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class HistoryComparisonSchema(BaseModel):
|
|
"""Response for event comparison."""
|
|
entity_id: UUID
|
|
entity_type: str
|
|
entity_name: str
|
|
event1: dict
|
|
event2: dict
|
|
differences: dict
|
|
changed_field_count: int
|
|
unchanged_field_count: int
|
|
time_between: str
|
|
|
|
|
|
class HistoryDiffCurrentSchema(BaseModel):
|
|
"""Response for comparing event with current state."""
|
|
entity_id: UUID
|
|
entity_type: str
|
|
entity_name: str
|
|
event: dict
|
|
current_state: dict
|
|
differences: dict
|
|
changed_field_count: int
|
|
time_since: str
|
|
|
|
|
|
class FieldHistorySchema(BaseModel):
|
|
"""Response for field-specific history."""
|
|
entity_id: UUID
|
|
entity_type: str
|
|
entity_name: str
|
|
field: str
|
|
field_type: str
|
|
history: List[dict]
|
|
total_changes: int
|
|
first_value: Optional[str] = None
|
|
current_value: Optional[str] = None
|
|
|
|
|
|
class HistoryActivitySummarySchema(BaseModel):
|
|
"""Response for activity summary."""
|
|
entity_id: UUID
|
|
entity_type: str
|
|
entity_name: str
|
|
total_events: int
|
|
accessible_events: int
|
|
summary: dict
|
|
most_changed_fields: Optional[List[dict]] = None
|
|
recent_activity: List[dict]
|
|
|
|
|
|
class RollbackRequestSchema(BaseModel):
|
|
"""Request body for rollback operation."""
|
|
fields: Optional[List[str]] = None
|
|
comment: str = ""
|
|
create_backup: bool = True
|
|
|
|
|
|
class RollbackResponseSchema(BaseModel):
|
|
"""Response for rollback operation."""
|
|
success: bool
|
|
message: str
|
|
entity_id: UUID
|
|
rollback_event_id: int
|
|
new_event_id: Optional[int]
|
|
fields_changed: dict
|
|
backup_event_id: Optional[int]
|
|
|
|
|
|
# ============================================================================
|
|
# Timeline Event Schemas
|
|
# ============================================================================
|
|
|
|
class EntityTimelineEventOut(BaseModel):
|
|
"""Schema for timeline event output."""
|
|
id: UUID
|
|
entity_id: UUID
|
|
entity_type: str
|
|
event_type: str
|
|
event_date: date
|
|
event_date_precision: Optional[str] = None
|
|
title: str
|
|
description: Optional[str] = None
|
|
from_entity_id: Optional[UUID] = None
|
|
to_entity_id: Optional[UUID] = None
|
|
from_location_id: Optional[UUID] = None
|
|
from_location_name: Optional[str] = None
|
|
to_location_id: Optional[UUID] = None
|
|
to_location_name: Optional[str] = None
|
|
from_value: Optional[str] = None
|
|
to_value: Optional[str] = None
|
|
is_public: bool
|
|
display_order: Optional[int] = None
|
|
created_by_id: Optional[UUID] = None
|
|
created_by_email: Optional[str] = None
|
|
approved_by_id: Optional[UUID] = None
|
|
approved_by_email: Optional[str] = None
|
|
submission_id: Optional[UUID] = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class EntityTimelineEventCreate(BaseModel):
|
|
"""Schema for creating a timeline event."""
|
|
entity_id: UUID = Field(..., description="ID of entity this event belongs to")
|
|
entity_type: str = Field(..., description="Type of entity (park, ride, company, ridemodel)")
|
|
event_type: str = Field(..., max_length=100, description="Type of event (opening, closing, relocation, etc.)")
|
|
event_date: date = Field(..., description="Date of the event")
|
|
event_date_precision: Optional[str] = Field('day', description="Precision: day, month, year, decade")
|
|
title: str = Field(..., min_length=1, max_length=255, description="Event title")
|
|
description: Optional[str] = Field(None, description="Detailed description")
|
|
from_entity_id: Optional[UUID] = Field(None, description="Source entity ID (for transfers/relocations)")
|
|
to_entity_id: Optional[UUID] = Field(None, description="Destination entity ID (for transfers/relocations)")
|
|
from_location_id: Optional[UUID] = Field(None, description="Source park ID (for relocations)")
|
|
to_location_id: Optional[UUID] = Field(None, description="Destination park ID (for relocations)")
|
|
from_value: Optional[str] = Field(None, description="Original value (for changes)")
|
|
to_value: Optional[str] = Field(None, description="New value (for changes)")
|
|
is_public: bool = Field(True, description="Whether event is publicly visible")
|
|
display_order: Optional[int] = Field(None, description="Display order")
|
|
|
|
@field_validator('entity_type')
|
|
def validate_entity_type(cls, v):
|
|
if v not in ['park', 'ride', 'company', 'ridemodel']:
|
|
raise ValueError('entity_type must be park, ride, company, or ridemodel')
|
|
return v
|
|
|
|
@field_validator('event_date_precision')
|
|
def validate_precision(cls, v):
|
|
if v and v not in ['day', 'month', 'year', 'decade']:
|
|
raise ValueError('event_date_precision must be day, month, year, or decade')
|
|
return v
|
|
|
|
|
|
class EntityTimelineEventUpdate(BaseModel):
|
|
"""Schema for updating a timeline event."""
|
|
event_type: Optional[str] = Field(None, max_length=100)
|
|
event_date: Optional[date] = None
|
|
event_date_precision: Optional[str] = None
|
|
title: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
description: Optional[str] = None
|
|
from_entity_id: Optional[UUID] = None
|
|
to_entity_id: Optional[UUID] = None
|
|
from_location_id: Optional[UUID] = None
|
|
to_location_id: Optional[UUID] = None
|
|
from_value: Optional[str] = None
|
|
to_value: Optional[str] = None
|
|
is_public: Optional[bool] = None
|
|
display_order: Optional[int] = None
|
|
|
|
|
|
class EntityTimelineEventListOut(BaseModel):
|
|
"""Paginated timeline event list response."""
|
|
items: List[EntityTimelineEventOut]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
total_pages: int
|
|
|
|
|
|
class TimelineStatsOut(BaseModel):
|
|
"""Statistics about timeline events."""
|
|
total_events: int
|
|
public_events: int
|
|
event_types: dict # {event_type: count}
|
|
earliest_event: Optional[date] = None
|
|
latest_event: Optional[date] = None
|
|
|
|
|
|
# ============================================================================
|
|
# Report Schemas
|
|
# ============================================================================
|
|
|
|
class ReportCreate(BaseModel):
|
|
"""Schema for creating a report."""
|
|
entity_type: str = Field(..., max_length=50, description="Type of entity being reported")
|
|
entity_id: UUID = Field(..., description="ID of entity being reported")
|
|
report_type: str = Field(..., description="Type of report: inappropriate, inaccurate, spam, duplicate, copyright, other")
|
|
description: str = Field(..., min_length=1, description="Description of the issue")
|
|
|
|
@field_validator('report_type')
|
|
def validate_report_type(cls, v):
|
|
valid_types = ['inappropriate', 'inaccurate', 'spam', 'duplicate', 'copyright', 'other']
|
|
if v not in valid_types:
|
|
raise ValueError(f'report_type must be one of: {", ".join(valid_types)}')
|
|
return v
|
|
|
|
|
|
class ReportUpdate(BaseModel):
|
|
"""Schema for updating a report (moderators only)."""
|
|
status: Optional[str] = Field(None, description="Status: pending, reviewing, resolved, dismissed")
|
|
reviewed_by_id: Optional[UUID] = None
|
|
reviewed_at: Optional[datetime] = None
|
|
resolution_notes: Optional[str] = Field(None, description="Moderation notes")
|
|
|
|
@field_validator('status')
|
|
def validate_status(cls, v):
|
|
if v and v not in ['pending', 'reviewing', 'resolved', 'dismissed']:
|
|
raise ValueError('status must be pending, reviewing, resolved, or dismissed')
|
|
return v
|
|
|
|
|
|
class ReportOut(BaseModel):
|
|
"""Schema for report output."""
|
|
id: UUID
|
|
entity_type: str
|
|
entity_id: UUID
|
|
report_type: str
|
|
description: str
|
|
status: str
|
|
reported_by_id: UUID
|
|
reported_by_email: Optional[str] = None
|
|
reviewed_by_id: Optional[UUID] = None
|
|
reviewed_by_email: Optional[str] = None
|
|
reviewed_at: Optional[datetime] = None
|
|
resolution_notes: Optional[str] = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class ReportListOut(BaseModel):
|
|
"""Paginated report list response."""
|
|
items: List[ReportOut]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
total_pages: int
|
|
|
|
|
|
class ReportStatsOut(BaseModel):
|
|
"""Statistics about reports."""
|
|
total_reports: int
|
|
pending_reports: int
|
|
reviewing_reports: int
|
|
resolved_reports: int
|
|
dismissed_reports: int
|
|
reports_by_type: dict # {report_type: count}
|
|
reports_by_entity_type: dict # {entity_type: count}
|
|
average_resolution_time_hours: Optional[float] = None
|
|
|
|
|
|
# ============================================================================
|
|
# Contact Schemas
|
|
# ============================================================================
|
|
|
|
class ContactSubmissionCreate(BaseModel):
|
|
"""Schema for creating a contact submission."""
|
|
name: str = Field(..., min_length=1, max_length=255, description="Contact name")
|
|
email: str = Field(..., description="Contact email address")
|
|
subject: str = Field(..., min_length=1, max_length=255, description="Subject")
|
|
message: str = Field(..., min_length=10, description="Message (min 10 characters)")
|
|
category: str = Field(..., description="Category: general, bug, feature, abuse, data, account, other")
|
|
|
|
@field_validator('category')
|
|
def validate_category(cls, v):
|
|
valid_categories = ['general', 'bug', 'feature', 'abuse', 'data', 'account', 'other']
|
|
if v not in valid_categories:
|
|
raise ValueError(f'category must be one of: {", ".join(valid_categories)}')
|
|
return v
|
|
|
|
|
|
class ContactSubmissionUpdate(BaseModel):
|
|
"""Schema for updating a contact submission (moderators only)."""
|
|
status: Optional[str] = Field(None, description="Status: pending, in_progress, resolved, archived")
|
|
assigned_to_id: Optional[UUID] = Field(None, description="User ID to assign to")
|
|
admin_notes: Optional[str] = Field(None, description="Internal admin notes")
|
|
|
|
@field_validator('status')
|
|
def validate_status(cls, v):
|
|
if v and v not in ['pending', 'in_progress', 'resolved', 'archived']:
|
|
raise ValueError('status must be pending, in_progress, resolved, or archived')
|
|
return v
|
|
|
|
|
|
class ContactSubmissionOut(BaseModel):
|
|
"""Schema for contact submission output."""
|
|
id: UUID
|
|
ticket_number: str
|
|
name: str
|
|
email: str
|
|
subject: str
|
|
message: str
|
|
category: str
|
|
status: str
|
|
user_id: Optional[UUID] = None
|
|
user_email: Optional[str] = None
|
|
assigned_to_id: Optional[UUID] = None
|
|
assigned_to_email: Optional[str] = None
|
|
admin_notes: Optional[str] = None
|
|
resolved_at: Optional[datetime] = None
|
|
resolved_by_id: Optional[UUID] = None
|
|
resolved_by_email: Optional[str] = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class ContactSubmissionListOut(BaseModel):
|
|
"""Paginated contact submission list response."""
|
|
items: List[ContactSubmissionOut]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
total_pages: int
|
|
|
|
|
|
class ContactSubmissionStatsOut(BaseModel):
|
|
"""Statistics about contact submissions."""
|
|
total_submissions: int
|
|
pending_submissions: int
|
|
in_progress_submissions: int
|
|
resolved_submissions: int
|
|
archived_submissions: int
|
|
submissions_by_category: dict # {category: count}
|
|
average_resolution_time_hours: Optional[float] = None
|
|
recent_submissions: List[ContactSubmissionOut]
|