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:
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