mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 04:31:13 -05:00
- Created a base email template (base.html) for consistent styling across all emails. - Added moderation approval email template (moderation_approved.html) to notify users of approved submissions. - Added moderation rejection email template (moderation_rejected.html) to inform users of required changes for their submissions. - Created password reset email template (password_reset.html) for users requesting to reset their passwords. - Developed a welcome email template (welcome.html) to greet new users and provide account details and tips for using ThrillWiki.
970 lines
30 KiB
Python
970 lines
30 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
|
|
|
|
|
|
# ============================================================================
|
|
# 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
|
|
|
|
|
|
# ============================================================================
|
|
# Versioning Schemas
|
|
# ============================================================================
|
|
|
|
class EntityVersionSchema(TimestampSchema):
|
|
"""Schema for entity version output."""
|
|
id: UUID
|
|
entity_type: str
|
|
entity_id: UUID
|
|
entity_name: str
|
|
version_number: int
|
|
change_type: str
|
|
snapshot: dict
|
|
changed_fields: dict
|
|
changed_by_id: Optional[UUID] = None
|
|
changed_by_email: Optional[str] = None
|
|
submission_id: Optional[UUID] = None
|
|
comment: Optional[str] = None
|
|
diff_summary: str
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class VersionHistoryResponseSchema(BaseModel):
|
|
"""Response schema for version history."""
|
|
entity_id: str
|
|
entity_type: str
|
|
entity_name: str
|
|
total_versions: int
|
|
versions: List[EntityVersionSchema]
|
|
|
|
|
|
class VersionDiffSchema(BaseModel):
|
|
"""Schema for version diff response."""
|
|
entity_id: str
|
|
entity_type: str
|
|
entity_name: str
|
|
version_number: int
|
|
version_date: datetime
|
|
differences: dict
|
|
changed_field_count: int
|
|
|
|
|
|
class VersionComparisonSchema(BaseModel):
|
|
"""Schema for comparing two versions."""
|
|
version1: EntityVersionSchema
|
|
version2: EntityVersionSchema
|
|
differences: dict
|
|
changed_field_count: 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)
|