POL-127: Add organizations table and championship ownership
- Create organizations table with Alembic migration (3-phase: create table, migrate data, drop old column) - Add org_id FK on championships linking to organizations - Refactor all schemas into one-class-per-file packages (auth, championship, organization, participant, registration, user) - Update CRUD layer with selectinload for organization relationships - Update frontend types and components to use nested organization object - Remove phantom Championship fields (subtitle, venue, accent_color) from frontend Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
from app.crud import crud_user, crud_championship, crud_registration, crud_participant
|
||||
from app.crud import crud_user, crud_organization, crud_championship, crud_registration, crud_participant
|
||||
|
||||
__all__ = ["crud_user", "crud_championship", "crud_registration", "crud_participant"]
|
||||
__all__ = ["crud_user", "crud_organization", "crud_championship", "crud_registration", "crud_participant"]
|
||||
|
||||
@@ -3,6 +3,7 @@ import uuid
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.models.championship import Championship
|
||||
from app.schemas.championship import ChampionshipCreate, ChampionshipUpdate
|
||||
@@ -16,7 +17,9 @@ def _serialize(value) -> str | None:
|
||||
|
||||
async def get(db: AsyncSession, champ_id: str | uuid.UUID) -> Championship | None:
|
||||
cid = champ_id if isinstance(champ_id, uuid.UUID) else uuid.UUID(str(champ_id))
|
||||
result = await db.execute(select(Championship).where(Championship.id == cid))
|
||||
result = await db.execute(
|
||||
select(Championship).where(Championship.id == cid).options(selectinload(Championship.organization))
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
@@ -26,7 +29,7 @@ async def list_all(
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> list[Championship]:
|
||||
q = select(Championship).order_by(Championship.event_date.asc())
|
||||
q = select(Championship).order_by(Championship.event_date.asc()).options(selectinload(Championship.organization))
|
||||
if status:
|
||||
q = q.where(Championship.status == status)
|
||||
q = q.offset(skip).limit(limit)
|
||||
@@ -34,14 +37,16 @@ async def list_all(
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def create(db: AsyncSession, data: ChampionshipCreate) -> Championship:
|
||||
async def create(db: AsyncSession, data: ChampionshipCreate, org_id: uuid.UUID | None = None) -> Championship:
|
||||
payload = data.model_dump(exclude={"judges", "categories"})
|
||||
payload["judges"] = _serialize(data.judges)
|
||||
payload["categories"] = _serialize(data.categories)
|
||||
if org_id:
|
||||
payload["org_id"] = org_id
|
||||
champ = Championship(**payload)
|
||||
db.add(champ)
|
||||
await db.commit()
|
||||
await db.refresh(champ)
|
||||
await db.refresh(champ, attribute_names=["organization"])
|
||||
return champ
|
||||
|
||||
|
||||
|
||||
11
backend/app/crud/crud_organization.py
Normal file
11
backend/app/crud/crud_organization.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.organization import Organization
|
||||
|
||||
|
||||
async def get_by_user_id(db: AsyncSession, user_id: uuid.UUID) -> Organization | None:
|
||||
result = await db.execute(select(Organization).where(Organization.user_id == user_id))
|
||||
return result.scalar_one_or_none()
|
||||
@@ -2,7 +2,9 @@ import uuid
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.models.organization import Organization
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserRegister, UserUpdate
|
||||
from app.services.auth_service import hash_password
|
||||
@@ -10,12 +12,16 @@ from app.services.auth_service import hash_password
|
||||
|
||||
async def get_by_id(db: AsyncSession, user_id: str | uuid.UUID) -> User | None:
|
||||
uid = user_id if isinstance(user_id, uuid.UUID) else uuid.UUID(str(user_id))
|
||||
result = await db.execute(select(User).where(User.id == uid))
|
||||
result = await db.execute(
|
||||
select(User).where(User.id == uid).options(selectinload(User.organization))
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_by_email(db: AsyncSession, email: str) -> User | None:
|
||||
result = await db.execute(select(User).where(User.email == email.lower()))
|
||||
result = await db.execute(
|
||||
select(User).where(User.email == email.lower()).options(selectinload(User.organization))
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
@@ -25,23 +31,40 @@ async def create(db: AsyncSession, data: UserRegister) -> User:
|
||||
hashed_password=hash_password(data.password),
|
||||
full_name=data.full_name,
|
||||
phone=data.phone,
|
||||
role=data.requested_role,
|
||||
organization_name=data.organization_name,
|
||||
instagram_handle=data.instagram_handle,
|
||||
role=data.requested_role,
|
||||
# Members are auto-approved; organizers require admin review
|
||||
status="approved" if data.requested_role == "member" else "pending",
|
||||
)
|
||||
db.add(user)
|
||||
await db.flush() # get user.id for the FK
|
||||
|
||||
# Create Organization row for organizer registrations
|
||||
if data.requested_role == "organizer" and data.organization_name:
|
||||
org = Organization(
|
||||
user_id=user.id,
|
||||
name=data.organization_name,
|
||||
status="pending",
|
||||
verified=False,
|
||||
)
|
||||
db.add(org)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
await db.refresh(user, attribute_names=["organization"])
|
||||
return user
|
||||
|
||||
|
||||
async def update(db: AsyncSession, user: User, data: UserUpdate) -> User:
|
||||
for field, value in data.model_dump(exclude_none=True).items():
|
||||
user_fields = data.model_dump(exclude_none=True, exclude={"organization_name"})
|
||||
for field, value in user_fields.items():
|
||||
setattr(user, field, value)
|
||||
|
||||
# Route org field updates to Organization table
|
||||
if data.organization_name is not None and user.organization:
|
||||
user.organization.name = data.organization_name
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
await db.refresh(user, attribute_names=["organization"])
|
||||
return user
|
||||
|
||||
|
||||
@@ -53,5 +76,7 @@ async def set_status(db: AsyncSession, user: User, status: str) -> User:
|
||||
|
||||
|
||||
async def list_all(db: AsyncSession, skip: int = 0, limit: int = 100) -> list[User]:
|
||||
result = await db.execute(select(User).offset(skip).limit(limit))
|
||||
result = await db.execute(
|
||||
select(User).options(selectinload(User.organization)).offset(skip).limit(limit)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from app.models.user import User, RefreshToken
|
||||
from app.models.organization import Organization
|
||||
from app.models.championship import Championship
|
||||
from app.models.registration import Registration
|
||||
from app.models.participant import ParticipantList
|
||||
@@ -7,6 +8,7 @@ from app.models.notification import NotificationLog
|
||||
__all__ = [
|
||||
"User",
|
||||
"RefreshToken",
|
||||
"Organization",
|
||||
"Championship",
|
||||
"Registration",
|
||||
"ParticipantList",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Float, Integer, String, Text, Uuid, func
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text, Uuid, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
@@ -11,6 +11,7 @@ class Championship(Base):
|
||||
__tablename__ = "championships"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
org_id: Mapped[uuid.UUID | None] = mapped_column(Uuid(as_uuid=True), ForeignKey("organizations.id", ondelete="SET NULL"))
|
||||
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
location: Mapped[str | None] = mapped_column(String(500))
|
||||
@@ -37,5 +38,6 @@ class Championship(Base):
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
organization: Mapped["Organization | None"] = relationship(back_populates="championships") # type: ignore[name-defined]
|
||||
registrations: Mapped[list["Registration"]] = relationship(back_populates="championship", cascade="all, delete-orphan") # type: ignore[name-defined]
|
||||
participant_list: Mapped["ParticipantList | None"] = relationship(back_populates="championship", uselist=False, cascade="all, delete-orphan") # type: ignore[name-defined]
|
||||
|
||||
@@ -15,8 +15,6 @@ class User(Base):
|
||||
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
full_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
phone: Mapped[str | None] = mapped_column(String(50))
|
||||
# Organizer-specific fields
|
||||
organization_name: Mapped[str | None] = mapped_column(String(255))
|
||||
instagram_handle: Mapped[str | None] = mapped_column(String(100))
|
||||
role: Mapped[str] = mapped_column(String(20), nullable=False, default="member")
|
||||
# 'pending' | 'approved' | 'rejected'
|
||||
@@ -30,6 +28,7 @@ class User(Base):
|
||||
refresh_tokens: Mapped[list["RefreshToken"]] = relationship(back_populates="user", cascade="all, delete-orphan")
|
||||
registrations: Mapped[list["Registration"]] = relationship(back_populates="user") # type: ignore[name-defined]
|
||||
notification_logs: Mapped[list["NotificationLog"]] = relationship(back_populates="user") # type: ignore[name-defined]
|
||||
organization: Mapped["Organization | None"] = relationship(back_populates="user", uselist=False) # type: ignore[name-defined]
|
||||
|
||||
|
||||
class RefreshToken(Base):
|
||||
|
||||
@@ -38,10 +38,11 @@ async def get_championship(
|
||||
@router.post("", response_model=ChampionshipOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_championship(
|
||||
data: ChampionshipCreate,
|
||||
_user: User = Depends(get_organizer),
|
||||
user: User = Depends(get_organizer),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await crud_championship.create(db, data)
|
||||
org_id = user.organization.id if user.organization else None
|
||||
return await crud_championship.create(db, data, org_id=org_id)
|
||||
|
||||
|
||||
@router.patch("/{champ_id}", response_model=ChampionshipOut)
|
||||
|
||||
7
backend/app/schemas/auth/__init__.py
Normal file
7
backend/app/schemas/auth/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from app.schemas.auth.token_pair import TokenPair
|
||||
from app.schemas.auth.refresh_request import RefreshRequest
|
||||
from app.schemas.auth.token_refreshed import TokenRefreshed
|
||||
from app.schemas.auth.logout_request import LogoutRequest
|
||||
from app.schemas.auth.register_response import RegisterResponse
|
||||
|
||||
__all__ = ["TokenPair", "RefreshRequest", "TokenRefreshed", "LogoutRequest", "RegisterResponse"]
|
||||
5
backend/app/schemas/auth/logout_request.py
Normal file
5
backend/app/schemas/auth/logout_request.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class LogoutRequest(BaseModel):
|
||||
refresh_token: str
|
||||
5
backend/app/schemas/auth/refresh_request.py
Normal file
5
backend/app/schemas/auth/refresh_request.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class RefreshRequest(BaseModel):
|
||||
refresh_token: str
|
||||
@@ -3,27 +3,6 @@ from pydantic import BaseModel
|
||||
from app.schemas.user import UserOut
|
||||
|
||||
|
||||
class TokenPair(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str = "bearer"
|
||||
user: UserOut
|
||||
|
||||
|
||||
class RefreshRequest(BaseModel):
|
||||
refresh_token: str
|
||||
|
||||
|
||||
class TokenRefreshed(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
|
||||
class LogoutRequest(BaseModel):
|
||||
refresh_token: str
|
||||
|
||||
|
||||
class RegisterResponse(BaseModel):
|
||||
"""
|
||||
Returned after registration.
|
||||
10
backend/app/schemas/auth/token_pair.py
Normal file
10
backend/app/schemas/auth/token_pair.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.schemas.user import UserOut
|
||||
|
||||
|
||||
class TokenPair(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str = "bearer"
|
||||
user: UserOut
|
||||
7
backend/app/schemas/auth/token_refreshed.py
Normal file
7
backend/app/schemas/auth/token_refreshed.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class TokenRefreshed(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str = "bearer"
|
||||
5
backend/app/schemas/championship/__init__.py
Normal file
5
backend/app/schemas/championship/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from app.schemas.championship.create import ChampionshipCreate
|
||||
from app.schemas.championship.update import ChampionshipUpdate
|
||||
from app.schemas.championship.out import ChampionshipOut
|
||||
|
||||
__all__ = ["ChampionshipCreate", "ChampionshipUpdate", "ChampionshipOut"]
|
||||
19
backend/app/schemas/championship/create.py
Normal file
19
backend/app/schemas/championship/create.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ChampionshipCreate(BaseModel):
|
||||
title: str
|
||||
description: str | None = None
|
||||
location: str | None = None
|
||||
event_date: datetime | None = None
|
||||
registration_open_at: datetime | None = None
|
||||
registration_close_at: datetime | None = None
|
||||
form_url: str | None = None
|
||||
entry_fee: float | None = None
|
||||
video_max_duration: int | None = None
|
||||
judges: list[dict] | None = None # [{name, bio, instagram}]
|
||||
categories: list[str] | None = None
|
||||
status: str = "draft"
|
||||
image_url: str | None = None
|
||||
@@ -4,43 +4,14 @@ from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, model_validator
|
||||
|
||||
|
||||
class ChampionshipCreate(BaseModel):
|
||||
title: str
|
||||
description: str | None = None
|
||||
location: str | None = None
|
||||
event_date: datetime | None = None
|
||||
registration_open_at: datetime | None = None
|
||||
registration_close_at: datetime | None = None
|
||||
form_url: str | None = None
|
||||
entry_fee: float | None = None
|
||||
video_max_duration: int | None = None
|
||||
judges: list[dict] | None = None # [{name, bio, instagram}]
|
||||
categories: list[str] | None = None
|
||||
status: str = "draft"
|
||||
image_url: str | None = None
|
||||
|
||||
|
||||
class ChampionshipUpdate(BaseModel):
|
||||
title: str | None = None
|
||||
description: str | None = None
|
||||
location: str | None = None
|
||||
event_date: datetime | None = None
|
||||
registration_open_at: datetime | None = None
|
||||
registration_close_at: datetime | None = None
|
||||
form_url: str | None = None
|
||||
entry_fee: float | None = None
|
||||
video_max_duration: int | None = None
|
||||
judges: list[dict] | None = None
|
||||
categories: list[str] | None = None
|
||||
status: str | None = None
|
||||
image_url: str | None = None
|
||||
from app.schemas.organization import OrganizationBrief
|
||||
|
||||
|
||||
class ChampionshipOut(BaseModel):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
id: uuid.UUID
|
||||
org_id: uuid.UUID | None = None
|
||||
title: str
|
||||
description: str | None
|
||||
location: str | None
|
||||
@@ -56,6 +27,7 @@ class ChampionshipOut(BaseModel):
|
||||
source: str
|
||||
instagram_media_id: str | None
|
||||
image_url: str | None
|
||||
organization: OrganizationBrief | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
19
backend/app/schemas/championship/update.py
Normal file
19
backend/app/schemas/championship/update.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ChampionshipUpdate(BaseModel):
|
||||
title: str | None = None
|
||||
description: str | None = None
|
||||
location: str | None = None
|
||||
event_date: datetime | None = None
|
||||
registration_open_at: datetime | None = None
|
||||
registration_close_at: datetime | None = None
|
||||
form_url: str | None = None
|
||||
entry_fee: float | None = None
|
||||
video_max_duration: int | None = None
|
||||
judges: list[dict] | None = None
|
||||
categories: list[str] | None = None
|
||||
status: str | None = None
|
||||
image_url: str | None = None
|
||||
4
backend/app/schemas/organization/__init__.py
Normal file
4
backend/app/schemas/organization/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from app.schemas.organization.out import OrganizationOut
|
||||
from app.schemas.organization.brief import OrganizationBrief
|
||||
|
||||
__all__ = ["OrganizationOut", "OrganizationBrief"]
|
||||
13
backend/app/schemas/organization/brief.py
Normal file
13
backend/app/schemas/organization/brief.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class OrganizationBrief(BaseModel):
|
||||
"""Minimal org info for embedding in ChampionshipOut."""
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
instagram: str | None
|
||||
logo_url: str | None
|
||||
16
backend/app/schemas/organization/out.py
Normal file
16
backend/app/schemas/organization/out.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class OrganizationOut(BaseModel):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
instagram: str | None
|
||||
email: str | None
|
||||
city: str | None
|
||||
logo_url: str | None
|
||||
verified: bool
|
||||
status: str
|
||||
4
backend/app/schemas/participant/__init__.py
Normal file
4
backend/app/schemas/participant/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from app.schemas.participant.out import ParticipantListOut
|
||||
from app.schemas.participant.publish import ParticipantListPublish
|
||||
|
||||
__all__ = ["ParticipantListOut", "ParticipantListPublish"]
|
||||
@@ -13,7 +13,3 @@ class ParticipantListOut(BaseModel):
|
||||
published_at: datetime | None
|
||||
notes: str | None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ParticipantListPublish(BaseModel):
|
||||
notes: str | None = None
|
||||
5
backend/app/schemas/participant/publish.py
Normal file
5
backend/app/schemas/participant/publish.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ParticipantListPublish(BaseModel):
|
||||
notes: str | None = None
|
||||
@@ -1,57 +0,0 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, model_validator
|
||||
|
||||
from app.schemas.user import UserOut
|
||||
|
||||
|
||||
class RegistrationCreate(BaseModel):
|
||||
championship_id: uuid.UUID
|
||||
category: str | None = None
|
||||
level: str | None = None
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
class RegistrationUpdate(BaseModel):
|
||||
status: str | None = None
|
||||
video_url: str | None = None
|
||||
category: str | None = None
|
||||
level: str | None = None
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
class RegistrationOut(BaseModel):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
id: uuid.UUID
|
||||
championship_id: uuid.UUID
|
||||
user_id: uuid.UUID
|
||||
category: str | None
|
||||
level: str | None
|
||||
notes: str | None
|
||||
status: str
|
||||
video_url: str | None
|
||||
submitted_at: datetime
|
||||
decided_at: datetime | None
|
||||
|
||||
|
||||
class RegistrationListItem(RegistrationOut):
|
||||
championship_title: str | None = None
|
||||
championship_event_date: datetime | None = None
|
||||
championship_location: str | None = None
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def extract_championship(cls, data: Any) -> Any:
|
||||
if hasattr(data, "championship") and data.championship:
|
||||
champ = data.championship
|
||||
data.__dict__["championship_title"] = champ.title
|
||||
data.__dict__["championship_event_date"] = champ.event_date
|
||||
data.__dict__["championship_location"] = champ.location
|
||||
return data
|
||||
|
||||
|
||||
class RegistrationWithUser(RegistrationOut):
|
||||
user: UserOut
|
||||
13
backend/app/schemas/registration/__init__.py
Normal file
13
backend/app/schemas/registration/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from app.schemas.registration.create import RegistrationCreate
|
||||
from app.schemas.registration.update import RegistrationUpdate
|
||||
from app.schemas.registration.out import RegistrationOut
|
||||
from app.schemas.registration.list_item import RegistrationListItem
|
||||
from app.schemas.registration.with_user import RegistrationWithUser
|
||||
|
||||
__all__ = [
|
||||
"RegistrationCreate",
|
||||
"RegistrationUpdate",
|
||||
"RegistrationOut",
|
||||
"RegistrationListItem",
|
||||
"RegistrationWithUser",
|
||||
]
|
||||
10
backend/app/schemas/registration/create.py
Normal file
10
backend/app/schemas/registration/create.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class RegistrationCreate(BaseModel):
|
||||
championship_id: uuid.UUID
|
||||
category: str | None = None
|
||||
level: str | None = None
|
||||
notes: str | None = None
|
||||
22
backend/app/schemas/registration/list_item.py
Normal file
22
backend/app/schemas/registration/list_item.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import model_validator
|
||||
|
||||
from app.schemas.registration.out import RegistrationOut
|
||||
|
||||
|
||||
class RegistrationListItem(RegistrationOut):
|
||||
championship_title: str | None = None
|
||||
championship_event_date: datetime | None = None
|
||||
championship_location: str | None = None
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def extract_championship(cls, data: Any) -> Any:
|
||||
if hasattr(data, "championship") and data.championship:
|
||||
champ = data.championship
|
||||
data.__dict__["championship_title"] = champ.title
|
||||
data.__dict__["championship_event_date"] = champ.event_date
|
||||
data.__dict__["championship_location"] = champ.location
|
||||
return data
|
||||
19
backend/app/schemas/registration/out.py
Normal file
19
backend/app/schemas/registration/out.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class RegistrationOut(BaseModel):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
id: uuid.UUID
|
||||
championship_id: uuid.UUID
|
||||
user_id: uuid.UUID
|
||||
category: str | None
|
||||
level: str | None
|
||||
notes: str | None
|
||||
status: str
|
||||
video_url: str | None
|
||||
submitted_at: datetime
|
||||
decided_at: datetime | None
|
||||
9
backend/app/schemas/registration/update.py
Normal file
9
backend/app/schemas/registration/update.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class RegistrationUpdate(BaseModel):
|
||||
status: str | None = None
|
||||
video_url: str | None = None
|
||||
category: str | None = None
|
||||
level: str | None = None
|
||||
notes: str | None = None
|
||||
6
backend/app/schemas/registration/with_user.py
Normal file
6
backend/app/schemas/registration/with_user.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from app.schemas.registration.out import RegistrationOut
|
||||
from app.schemas.user import UserOut
|
||||
|
||||
|
||||
class RegistrationWithUser(RegistrationOut):
|
||||
user: UserOut
|
||||
6
backend/app/schemas/user/__init__.py
Normal file
6
backend/app/schemas/user/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from app.schemas.user.register import UserRegister
|
||||
from app.schemas.user.login import UserLogin
|
||||
from app.schemas.user.out import UserOut
|
||||
from app.schemas.user.update import UserUpdate
|
||||
|
||||
__all__ = ["UserRegister", "UserLogin", "UserOut", "UserUpdate"]
|
||||
6
backend/app/schemas/user/login.py
Normal file
6
backend/app/schemas/user/login.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
21
backend/app/schemas/user/out.py
Normal file
21
backend/app/schemas/user/out.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.schemas.organization import OrganizationOut
|
||||
|
||||
|
||||
class UserOut(BaseModel):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
id: uuid.UUID
|
||||
email: str
|
||||
full_name: str
|
||||
phone: str | None
|
||||
role: str
|
||||
status: str
|
||||
instagram_handle: str | None
|
||||
organization: OrganizationOut | None = None
|
||||
expo_push_token: str | None
|
||||
created_at: datetime
|
||||
@@ -1,5 +1,3 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, EmailStr, field_validator
|
||||
@@ -22,31 +20,3 @@ class UserRegister(BaseModel):
|
||||
if info.data.get("requested_role") == "organizer" and not v:
|
||||
raise ValueError("Organization name is required for organizer registration")
|
||||
return v
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
|
||||
class UserOut(BaseModel):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
id: uuid.UUID
|
||||
email: str
|
||||
full_name: str
|
||||
phone: str | None
|
||||
role: str
|
||||
status: str
|
||||
organization_name: str | None
|
||||
instagram_handle: str | None
|
||||
expo_push_token: str | None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
full_name: str | None = None
|
||||
phone: str | None = None
|
||||
organization_name: str | None = None
|
||||
instagram_handle: str | None = None
|
||||
expo_push_token: str | None = None
|
||||
10
backend/app/schemas/user/update.py
Normal file
10
backend/app/schemas/user/update.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
full_name: str | None = None
|
||||
phone: str | None = None
|
||||
instagram_handle: str | None = None
|
||||
expo_push_token: str | None = None
|
||||
# Org fields — routed to Organization table in CRUD
|
||||
organization_name: str | None = None
|
||||
Reference in New Issue
Block a user