mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -05:00
Add email templates for user notifications and account management
- 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.
This commit is contained in:
969
django/api/v1/schemas.py
Normal file
969
django/api/v1/schemas.py
Normal file
@@ -0,0 +1,969 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user