Initial commit: Pole Dance Championships App

Full-stack mobile app for pole dance championship management.

Backend: FastAPI + SQLAlchemy 2 (async) + SQLite (dev) / PostgreSQL (prod)
- JWT auth with refresh token rotation
- Championship CRUD with Instagram Graph API sync (APScheduler)
- Registration flow with status management
- Participant list publish with Expo push notifications
- Alembic migrations, pytest test suite

Mobile: React Native + Expo (TypeScript)
- Auth gate: pending approval screen for new members
- Championships list & detail screens
- Registration form with status tracking
- React Query + Zustand + React Navigation v6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dianaka123
2026-02-22 22:47:10 +03:00
commit 1c5719ac85
54 changed files with 2383 additions and 0 deletions

View File

View File

@@ -0,0 +1,64 @@
import uuid
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.championship import Championship
def _uuid(v: str | uuid.UUID) -> uuid.UUID:
return v if isinstance(v, uuid.UUID) else uuid.UUID(str(v))
async def get(db: AsyncSession, championship_id: str | uuid.UUID) -> Championship | None:
result = await db.execute(
select(Championship).where(Championship.id == _uuid(championship_id))
)
return result.scalar_one_or_none()
async def get_by_instagram_id(
db: AsyncSession, instagram_media_id: str
) -> Championship | None:
result = await db.execute(
select(Championship).where(
Championship.instagram_media_id == instagram_media_id
)
)
return result.scalar_one_or_none()
async def list_all(
db: AsyncSession,
status: str | None = None,
skip: int = 0,
limit: int = 20,
) -> list[Championship]:
q = select(Championship)
if status:
q = q.where(Championship.status == status)
q = q.order_by(Championship.event_date.asc().nullslast()).offset(skip).limit(limit)
result = await db.execute(q)
return list(result.scalars().all())
async def create(db: AsyncSession, **kwargs) -> Championship:
champ = Championship(**kwargs)
db.add(champ)
await db.commit()
await db.refresh(champ)
return champ
async def update(db: AsyncSession, champ: Championship, **kwargs) -> Championship:
for key, value in kwargs.items():
if value is not None:
setattr(champ, key, value)
await db.commit()
await db.refresh(champ)
return champ
async def delete(db: AsyncSession, champ: Championship) -> None:
await db.delete(champ)
await db.commit()

View File

@@ -0,0 +1,62 @@
import uuid
from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.participant_list import ParticipantList
def _uuid(v: str | uuid.UUID) -> uuid.UUID:
return v if isinstance(v, uuid.UUID) else uuid.UUID(str(v))
async def get_by_championship(
db: AsyncSession, championship_id: str | uuid.UUID
) -> ParticipantList | None:
result = await db.execute(
select(ParticipantList).where(
ParticipantList.championship_id == _uuid(championship_id)
)
)
return result.scalar_one_or_none()
async def upsert(
db: AsyncSession,
championship_id: uuid.UUID,
published_by: uuid.UUID,
notes: str | None,
) -> ParticipantList:
existing = await get_by_championship(db, championship_id)
if existing:
existing.notes = notes
existing.published_by = published_by
await db.commit()
await db.refresh(existing)
return existing
pl = ParticipantList(
championship_id=championship_id,
published_by=published_by,
notes=notes,
)
db.add(pl)
await db.commit()
await db.refresh(pl)
return pl
async def publish(db: AsyncSession, pl: ParticipantList) -> ParticipantList:
pl.is_published = True
pl.published_at = datetime.now(timezone.utc)
await db.commit()
await db.refresh(pl)
return pl
async def unpublish(db: AsyncSession, pl: ParticipantList) -> ParticipantList:
pl.is_published = False
pl.published_at = None
await db.commit()
await db.refresh(pl)
return pl

View File

@@ -0,0 +1,43 @@
import uuid
from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.refresh_token import RefreshToken
async def create(
db: AsyncSession,
user_id: uuid.UUID,
token_hash: str,
expires_at: datetime,
) -> RefreshToken:
rt = RefreshToken(user_id=user_id, token_hash=token_hash, expires_at=expires_at)
db.add(rt)
await db.commit()
await db.refresh(rt)
return rt
async def get_by_hash(db: AsyncSession, token_hash: str) -> RefreshToken | None:
result = await db.execute(
select(RefreshToken).where(RefreshToken.token_hash == token_hash)
)
return result.scalar_one_or_none()
async def revoke(db: AsyncSession, rt: RefreshToken) -> None:
rt.revoked = True
await db.commit()
def is_valid(rt: RefreshToken) -> bool:
if rt.revoked:
return False
now = datetime.now(timezone.utc)
expires = rt.expires_at
# SQLite returns naive datetimes; normalise to UTC-aware for comparison
if expires.tzinfo is None:
expires = expires.replace(tzinfo=timezone.utc)
return expires > now

View File

@@ -0,0 +1,86 @@
import uuid
from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.registration import Registration
def _uuid(v: str | uuid.UUID) -> uuid.UUID:
return v if isinstance(v, uuid.UUID) else uuid.UUID(str(v))
async def get(db: AsyncSession, registration_id: str | uuid.UUID) -> Registration | None:
result = await db.execute(
select(Registration).where(Registration.id == _uuid(registration_id))
)
return result.scalar_one_or_none()
async def get_by_champ_and_user(
db: AsyncSession, championship_id: str | uuid.UUID, user_id: str | uuid.UUID
) -> Registration | None:
result = await db.execute(
select(Registration).where(
Registration.championship_id == _uuid(championship_id),
Registration.user_id == _uuid(user_id),
)
)
return result.scalar_one_or_none()
async def list_by_user(db: AsyncSession, user_id: str | uuid.UUID) -> list[Registration]:
result = await db.execute(
select(Registration)
.where(Registration.user_id == _uuid(user_id))
.order_by(Registration.submitted_at.desc())
)
return list(result.scalars().all())
async def list_by_championship(
db: AsyncSession, championship_id: str | uuid.UUID
) -> list[Registration]:
result = await db.execute(
select(Registration)
.where(Registration.championship_id == _uuid(championship_id))
.order_by(Registration.submitted_at.asc())
)
return list(result.scalars().all())
async def create(
db: AsyncSession,
championship_id: uuid.UUID,
user_id: uuid.UUID,
category: str | None,
level: str | None,
notes: str | None,
) -> Registration:
reg = Registration(
championship_id=championship_id,
user_id=user_id,
category=category,
level=level,
notes=notes,
)
db.add(reg)
await db.commit()
await db.refresh(reg)
return reg
async def update_status(
db: AsyncSession, reg: Registration, status: str
) -> Registration:
reg.status = status
reg.decided_at = datetime.now(timezone.utc)
await db.commit()
await db.refresh(reg)
return reg
async def delete(db: AsyncSession, reg: Registration) -> None:
await db.delete(reg)
await db.commit()

View File

@@ -0,0 +1,72 @@
import uuid
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.user import User
from app.services.auth_service import hash_password
async def get_by_email(db: AsyncSession, email: str) -> User | None:
result = await db.execute(select(User).where(User.email == email))
return result.scalar_one_or_none()
async def get(db: AsyncSession, user_id: str | uuid.UUID) -> User | None:
uid = uuid.UUID(str(user_id)) if not isinstance(user_id, uuid.UUID) else user_id
result = await db.execute(select(User).where(User.id == uid))
return result.scalar_one_or_none()
async def create(
db: AsyncSession,
email: str,
password: str,
full_name: str,
phone: str | None = None,
role: str = "member",
status: str = "pending",
) -> User:
user = User(
email=email,
hashed_password=hash_password(password),
full_name=full_name,
phone=phone,
role=role,
status=status,
)
db.add(user)
await db.commit()
await db.refresh(user)
return user
async def list_all(
db: AsyncSession,
status: str | None = None,
role: str | None = None,
skip: int = 0,
limit: int = 50,
) -> list[User]:
q = select(User)
if status:
q = q.where(User.status == status)
if role:
q = q.where(User.role == role)
q = q.offset(skip).limit(limit)
result = await db.execute(q)
return list(result.scalars().all())
async def set_status(db: AsyncSession, user: User, status: str) -> User:
user.status = status
await db.commit()
await db.refresh(user)
return user
async def set_push_token(db: AsyncSession, user: User, token: str) -> User:
user.expo_push_token = token
await db.commit()
await db.refresh(user)
return user