15 phase 3 tournament add
This commit is contained in:
@@ -34,3 +34,14 @@ async def get_current_user(
|
|||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_superuser(
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
) -> User:
|
||||||
|
if not current_user.is_superuser:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not enough privileges",
|
||||||
|
)
|
||||||
|
return current_user
|
||||||
|
|||||||
76
backend/api/routers/tournaments.py
Normal file
76
backend/api/routers/tournaments.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from api.deps import get_current_superuser
|
||||||
|
from database.models import Seat, Tournament, User
|
||||||
|
from database.session import get_db
|
||||||
|
from schemas.tournament import SeatGenerateRequest, TournamentCreate, TournamentResponse
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/tournaments", tags=["tournaments"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=TournamentResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_tournament(
|
||||||
|
body: TournamentCreate,
|
||||||
|
current_user: User = Depends(get_current_superuser),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
) -> Tournament:
|
||||||
|
tournament = Tournament(
|
||||||
|
title=body.title,
|
||||||
|
description=body.description,
|
||||||
|
event_date=body.event_date,
|
||||||
|
)
|
||||||
|
db.add(tournament)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(tournament)
|
||||||
|
return tournament
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/{tournament_id}/generate-seats",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
async def generate_seats(
|
||||||
|
tournament_id: int,
|
||||||
|
request: SeatGenerateRequest,
|
||||||
|
current_user: User = Depends(get_current_superuser),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
) -> dict[str, str]:
|
||||||
|
# 1. Проверяем существование турнира
|
||||||
|
result = await db.execute(select(Tournament).where(Tournament.id == tournament_id))
|
||||||
|
tournament: Tournament | None = result.scalar_one_or_none()
|
||||||
|
if tournament is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tournament not found")
|
||||||
|
|
||||||
|
# 2. Проверяем, что места ещё не генерировались (защита от дублей)
|
||||||
|
count_result = await db.execute(
|
||||||
|
select(func.count()).where(Seat.tournament_id == tournament_id)
|
||||||
|
)
|
||||||
|
existing_count: int = count_result.scalar_one()
|
||||||
|
if existing_count > 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Места уже сгенерированы",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Генерируем места для каждого сектора
|
||||||
|
seats: list[Seat] = []
|
||||||
|
for sector_cfg in request.sectors:
|
||||||
|
for row in range(1, sector_cfg.rows + 1):
|
||||||
|
for number in range(1, sector_cfg.seats_per_row + 1):
|
||||||
|
seats.append(
|
||||||
|
Seat(
|
||||||
|
tournament_id=tournament_id,
|
||||||
|
sector=sector_cfg.sector_name,
|
||||||
|
row=row,
|
||||||
|
number=number,
|
||||||
|
price=sector_cfg.price,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Массовая вставка
|
||||||
|
db.add_all(seats)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
return {"message": f"Generated {len(seats)} seats successfully"}
|
||||||
@@ -18,12 +18,14 @@ class User(Base):
|
|||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
email: Mapped[str] = mapped_column(String, unique=True, index=True)
|
email: Mapped[str] = mapped_column(String, unique=True, index=True)
|
||||||
hashed_password: Mapped[str] = mapped_column(String)
|
hashed_password: Mapped[str] = mapped_column(String)
|
||||||
|
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
tickets: Mapped[list["Ticket"]] = relationship(back_populates="user")
|
tickets: Mapped[list["Ticket"]] = relationship(back_populates="user")
|
||||||
|
|
||||||
class Tournament(Base):
|
class Tournament(Base):
|
||||||
__tablename__ = "tournaments"
|
__tablename__ = "tournaments"
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
title: Mapped[str] = mapped_column(String)
|
title: Mapped[str] = mapped_column(String)
|
||||||
|
description: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||||
event_date: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
event_date: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
seats: Mapped[list["Seat"]] = relationship(back_populates="tournament")
|
seats: Mapped[list["Seat"]] = relationship(back_populates="tournament")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from core.redis import acquire_seat_lock, release_seat_lock
|
|||||||
from api.routers.auth import router as auth_router
|
from api.routers.auth import router as auth_router
|
||||||
from api.routers.webhooks import router as webhooks_router
|
from api.routers.webhooks import router as webhooks_router
|
||||||
from api.routers.tickets import router as tickets_router
|
from api.routers.tickets import router as tickets_router
|
||||||
|
from api.routers.tournaments import router as tournaments_router
|
||||||
|
|
||||||
app = FastAPI(title="Ticketing System API")
|
app = FastAPI(title="Ticketing System API")
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ app.add_middleware(
|
|||||||
app.include_router(auth_router)
|
app.include_router(auth_router)
|
||||||
app.include_router(webhooks_router)
|
app.include_router(webhooks_router)
|
||||||
app.include_router(tickets_router)
|
app.include_router(tickets_router)
|
||||||
|
app.include_router(tournaments_router)
|
||||||
|
|
||||||
@app.post("/api/seats/{seat_id}/lock", status_code=status.HTTP_200_OK)
|
@app.post("/api/seats/{seat_id}/lock", status_code=status.HTTP_200_OK)
|
||||||
async def lock_seat(seat_id: int, user_id: int, db: AsyncSession = Depends(get_db)):
|
async def lock_seat(seat_id: int, user_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""add is_superuser to users
|
||||||
|
|
||||||
|
Revision ID: c82cc216a199
|
||||||
|
Revises: a55d80c4b300
|
||||||
|
Create Date: 2026-03-06 16:26:44.600332
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'c82cc216a199'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = 'a55d80c4b300'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('users', sa.Column('is_superuser', sa.Boolean(), server_default='false', nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('users', 'is_superuser')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""add_tournament_description
|
||||||
|
|
||||||
|
Revision ID: d096f9d0b612
|
||||||
|
Revises: c82cc216a199
|
||||||
|
Create Date: 2026-03-06 17:18:22.377857
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'd096f9d0b612'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = 'c82cc216a199'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('tournaments', sa.Column('description', sa.String(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('tournaments', 'description')
|
||||||
|
# ### end Alembic commands ###
|
||||||
36
backend/schemas/tournament.py
Normal file
36
backend/schemas/tournament.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, field_validator
|
||||||
|
|
||||||
|
|
||||||
|
class SectorConfig(BaseModel):
|
||||||
|
sector_name: str
|
||||||
|
rows: int
|
||||||
|
seats_per_row: int
|
||||||
|
price: int
|
||||||
|
|
||||||
|
@field_validator("rows", "seats_per_row", "price")
|
||||||
|
@classmethod
|
||||||
|
def must_be_positive(cls, v: int) -> int:
|
||||||
|
if v <= 0:
|
||||||
|
raise ValueError("Must be a positive integer")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class SeatGenerateRequest(BaseModel):
|
||||||
|
sectors: list[SectorConfig]
|
||||||
|
|
||||||
|
|
||||||
|
class TournamentCreate(BaseModel):
|
||||||
|
title: str
|
||||||
|
description: str | None = None
|
||||||
|
event_date: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class TournamentResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
description: str | None
|
||||||
|
event_date: datetime
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
Reference in New Issue
Block a user