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:
Dianaka123
2026-03-01 22:09:10 +03:00
parent 96e02bf64a
commit d4f0a05707
44 changed files with 450 additions and 183 deletions

View File

@@ -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())