This commit is contained in:
Artem Tsyrulnikov
2026-04-06 19:27:27 +03:00
commit 477ddb8bf9
38 changed files with 1916 additions and 0 deletions

0
app/__init__.py Normal file
View File

0
app/auth/__init__.py Normal file
View File

49
app/auth/dependencies.py Normal file
View File

@@ -0,0 +1,49 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlmodel import Session, select
from app.connection import get_session
from app.models.user import User
from app.auth.utils import decode_access_token
security = HTTPBearer()
def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
session: Session = Depends(get_session),
) -> User:
"""Извлекает текущего пользователя из JWT-токена."""
payload = decode_access_token(credentials.credentials)
if payload is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Невалидный токен",
)
user_id: int | None = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Невалидный токен",
)
user = session.get(User, int(user_id))
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Пользователь не найден",
)
return user
def get_current_organizer(
current_user: User = Depends(get_current_user),
) -> User:
"""Проверяет, что текущий пользователь — организатор."""
if not current_user.is_organizer:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Доступно только организаторам",
)
return current_user

44
app/auth/router.py Normal file
View File

@@ -0,0 +1,44 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.connection import get_session
from app.models.user import User
from app.schemas.user import UserCreate, UserRead, Token, LoginRequest
from app.auth.utils import hash_password, verify_password, create_access_token
router = APIRouter(prefix="/auth", tags=["Auth"])
@router.post("/register", response_model=UserRead, status_code=status.HTTP_201_CREATED)
def register(data: UserCreate, session: Session = Depends(get_session)) -> User:
existing = session.exec(select(User).where(User.email == data.email)).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Пользователь с таким email уже существует",
)
user = User(
name=data.name,
email=data.email,
phone=data.phone,
hashed_password=hash_password(data.password),
is_organizer=data.is_organizer,
)
session.add(user)
session.commit()
session.refresh(user)
return user
@router.post("/login", response_model=Token)
def login(data: LoginRequest, session: Session = Depends(get_session)) -> dict:
user = session.exec(select(User).where(User.email == data.email)).first()
if not user or not verify_password(data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверный email или пароль",
)
access_token = create_access_token({"sub": str(user.id)})
return {"access_token": access_token, "token_type": "bearer"}

29
app/auth/utils.py Normal file
View File

@@ -0,0 +1,29 @@
from datetime import datetime, timedelta, timezone
import bcrypt
import jwt
from app.config import settings
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
def verify_password(plain_password: str, hashed_password: str) -> bool:
return bcrypt.checkpw(plain_password.encode(), hashed_password.encode())
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
def decode_access_token(token: str) -> dict | None:
"""Декодирует JWT-токен. Возвращает payload или None при ошибке."""
try:
return jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
except jwt.PyJWTError:
return None

13
app/config.py Normal file
View File

@@ -0,0 +1,13 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str = "postgresql://postgres:postgres@localhost:5432/hackathon_db"
secret_key: str = "super-secret-key-change-in-production"
algorithm: str = "HS256"
access_token_expire_minutes: int = 30
model_config = {"env_prefix": ""}
settings = Settings()

14
app/connection.py Normal file
View File

@@ -0,0 +1,14 @@
from sqlmodel import SQLModel, Session, create_engine
from app.config import settings
engine = create_engine(settings.database_url, echo=True)
def init_db():
SQLModel.metadata.create_all(engine)
def get_session():
with Session(engine) as session:
yield session

24
app/main.py Normal file
View File

@@ -0,0 +1,24 @@
from fastapi import FastAPI
from app.connection import init_db
from app.auth.router import router as auth_router
from app.routers.users import router as users_router
from app.routers.hackathons import router as hackathons_router
from app.routers.teams import router as teams_router
from app.routers.tasks import router as tasks_router
from app.routers.submissions import router as submissions_router
app = FastAPI(title="Hackathon Platform API", version="1.0.0")
@app.on_event("startup")
def on_startup():
init_db()
app.include_router(auth_router)
app.include_router(users_router)
app.include_router(hackathons_router)
app.include_router(teams_router)
app.include_router(tasks_router)
app.include_router(submissions_router)

17
app/models/__init__.py Normal file
View File

@@ -0,0 +1,17 @@
from app.models.user import User
from app.models.hackathon import Hackathon
from app.models.hackathon_participant import HackathonParticipant
from app.models.team import Team
from app.models.team_member import TeamMember
from app.models.task import Task
from app.models.submission import Submission
__all__ = [
"User",
"Hackathon",
"HackathonParticipant",
"Team",
"TeamMember",
"Task",
"Submission",
]

29
app/models/hackathon.py Normal file
View File

@@ -0,0 +1,29 @@
from datetime import datetime
from sqlmodel import SQLModel, Field, Relationship
class Hackathon(SQLModel, table=True):
__tablename__ = "hackathons"
id: int | None = Field(default=None, primary_key=True)
title: str = Field(max_length=200)
description: str
start_date: datetime
end_date: datetime
organizer_id: int = Field(foreign_key="users.id")
# Relationships
organizer: "User" = Relationship(back_populates="organized_hackathons")
participants: list["HackathonParticipant"] = Relationship(
back_populates="hackathon",
sa_relationship_kwargs={"cascade": "all, delete"},
)
teams: list["Team"] = Relationship(
back_populates="hackathon",
sa_relationship_kwargs={"cascade": "all, delete"},
)
tasks: list["Task"] = Relationship(
back_populates="hackathon",
sa_relationship_kwargs={"cascade": "all, delete"},
)

View File

@@ -0,0 +1,16 @@
from sqlmodel import SQLModel, Field, Relationship
class HackathonParticipant(SQLModel, table=True):
"""Регистрация участника на хакатон (many-to-many: User <-> Hackathon)."""
__tablename__ = "hackathon_participants"
id: int | None = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="users.id")
hackathon_id: int = Field(foreign_key="hackathons.id")
is_confirmed: bool = Field(default=False)
# Relationships
user: "User" = Relationship(back_populates="registrations")
hackathon: "Hackathon" = Relationship(back_populates="participants")

18
app/models/submission.py Normal file
View File

@@ -0,0 +1,18 @@
from datetime import datetime
from sqlmodel import SQLModel, Field, Relationship
class Submission(SQLModel, table=True):
__tablename__ = "submissions"
id: int | None = Field(default=None, primary_key=True)
team_id: int = Field(foreign_key="teams.id")
task_id: int = Field(foreign_key="tasks.id")
description: str
file_url: str | None = None
submitted_at: datetime = Field(default_factory=datetime.utcnow)
# Relationships
team: "Team" = Relationship(back_populates="submissions")
task: "Task" = Relationship(back_populates="submissions")

19
app/models/task.py Normal file
View File

@@ -0,0 +1,19 @@
from sqlmodel import SQLModel, Field, Relationship
class Task(SQLModel, table=True):
__tablename__ = "tasks"
id: int | None = Field(default=None, primary_key=True)
hackathon_id: int = Field(foreign_key="hackathons.id")
title: str = Field(max_length=200)
description: str
requirements: str
evaluation_criteria: str
# Relationships
hackathon: "Hackathon" = Relationship(back_populates="tasks")
submissions: list["Submission"] = Relationship(
back_populates="task",
sa_relationship_kwargs={"cascade": "all, delete"},
)

20
app/models/team.py Normal file
View File

@@ -0,0 +1,20 @@
from sqlmodel import SQLModel, Field, Relationship
class Team(SQLModel, table=True):
__tablename__ = "teams"
id: int | None = Field(default=None, primary_key=True)
name: str = Field(max_length=100)
hackathon_id: int = Field(foreign_key="hackathons.id")
# Relationships
hackathon: "Hackathon" = Relationship(back_populates="teams")
members: list["TeamMember"] = Relationship(
back_populates="team",
sa_relationship_kwargs={"cascade": "all, delete"},
)
submissions: list["Submission"] = Relationship(
back_populates="team",
sa_relationship_kwargs={"cascade": "all, delete"},
)

16
app/models/team_member.py Normal file
View File

@@ -0,0 +1,16 @@
from sqlmodel import SQLModel, Field, Relationship
class TeamMember(SQLModel, table=True):
"""Участник команды (many-to-many: User <-> Team)."""
__tablename__ = "team_members"
id: int | None = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="users.id")
team_id: int = Field(foreign_key="teams.id")
role: str = Field(default="participant", max_length=50) # programmer, designer, marketer...
# Relationships
user: "User" = Relationship(back_populates="team_memberships")
team: "Team" = Relationship(back_populates="members")

26
app/models/user.py Normal file
View File

@@ -0,0 +1,26 @@
from sqlmodel import SQLModel, Field, Relationship
class User(SQLModel, table=True):
__tablename__ = "users"
id: int | None = Field(default=None, primary_key=True)
name: str = Field(max_length=100)
email: str = Field(max_length=255, unique=True, index=True)
phone: str | None = Field(default=None, max_length=20)
hashed_password: str
is_organizer: bool = Field(default=False)
# Relationships
organized_hackathons: list["Hackathon"] = Relationship(
back_populates="organizer",
sa_relationship_kwargs={"cascade": "all, delete"},
)
registrations: list["HackathonParticipant"] = Relationship(
back_populates="user",
sa_relationship_kwargs={"cascade": "all, delete"},
)
team_memberships: list["TeamMember"] = Relationship(
back_populates="user",
sa_relationship_kwargs={"cascade": "all, delete"},
)

0
app/routers/__init__.py Normal file
View File

127
app/routers/hackathons.py Normal file
View File

@@ -0,0 +1,127 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.connection import get_session
from app.models.hackathon import Hackathon
from app.models.hackathon_participant import HackathonParticipant
from app.models.user import User
from app.schemas.hackathon import (
HackathonCreate,
HackathonRead,
HackathonReadWithOrganizer,
ParticipantRegistration,
ParticipantRead,
ParticipantConfirm,
)
from app.auth.dependencies import get_current_user, get_current_organizer
router = APIRouter(prefix="/hackathons", tags=["Hackathons"])
# ── CRUD ────────────────────────────────────────────────────────────────────
@router.post("/", response_model=HackathonRead, status_code=status.HTTP_201_CREATED)
def create_hackathon(
data: HackathonCreate,
session: Session = Depends(get_session),
organizer: User = Depends(get_current_organizer),
) -> Hackathon:
hackathon = Hackathon(**data.model_dump(), organizer_id=organizer.id)
session.add(hackathon)
session.commit()
session.refresh(hackathon)
return hackathon
@router.get("/", response_model=list[HackathonRead])
def list_hackathons(session: Session = Depends(get_session)) -> list[Hackathon]:
return list(session.exec(select(Hackathon)).all())
@router.get("/{hackathon_id}", response_model=HackathonReadWithOrganizer)
def get_hackathon(hackathon_id: int, session: Session = Depends(get_session)) -> Hackathon:
hackathon = session.get(Hackathon, hackathon_id)
if not hackathon:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Хакатон не найден")
return hackathon
@router.delete("/{hackathon_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_hackathon(
hackathon_id: int,
session: Session = Depends(get_session),
organizer: User = Depends(get_current_organizer),
) -> None:
hackathon = session.get(Hackathon, hackathon_id)
if not hackathon:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Хакатон не найден")
session.delete(hackathon)
session.commit()
# ── Регистрация участников ──────────────────────────────────────────────────
@router.post("/{hackathon_id}/register", response_model=ParticipantRead, status_code=status.HTTP_201_CREATED)
def register_for_hackathon(
hackathon_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
) -> HackathonParticipant:
hackathon = session.get(Hackathon, hackathon_id)
if not hackathon:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Хакатон не найден")
existing = session.exec(
select(HackathonParticipant).where(
HackathonParticipant.user_id == current_user.id,
HackathonParticipant.hackathon_id == hackathon_id,
)
).first()
if existing:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Вы уже зарегистрированы")
participant = HackathonParticipant(
user_id=current_user.id,
hackathon_id=hackathon_id,
)
session.add(participant)
session.commit()
session.refresh(participant)
return participant
@router.get("/{hackathon_id}/participants", response_model=list[ParticipantRead])
def list_participants(
hackathon_id: int,
session: Session = Depends(get_session),
) -> list[HackathonParticipant]:
return list(
session.exec(
select(HackathonParticipant).where(HackathonParticipant.hackathon_id == hackathon_id)
).all()
)
@router.patch("/{hackathon_id}/confirm", response_model=ParticipantRead)
def confirm_participant(
hackathon_id: int,
data: ParticipantConfirm,
session: Session = Depends(get_session),
organizer: User = Depends(get_current_organizer),
) -> HackathonParticipant:
"""Организатор подтверждает регистрацию участника."""
participant = session.exec(
select(HackathonParticipant).where(
HackathonParticipant.user_id == data.user_id,
HackathonParticipant.hackathon_id == hackathon_id,
)
).first()
if not participant:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Регистрация не найдена")
participant.is_confirmed = True
session.add(participant)
session.commit()
session.refresh(participant)
return participant

View File

@@ -0,0 +1,59 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.connection import get_session
from app.models.submission import Submission
from app.models.user import User
from app.schemas.submission import SubmissionCreate, SubmissionRead, SubmissionReadWithDetails
from app.auth.dependencies import get_current_user
router = APIRouter(prefix="/submissions", tags=["Submissions"])
@router.post("/", response_model=SubmissionRead, status_code=status.HTTP_201_CREATED)
def create_submission(
data: SubmissionCreate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
) -> Submission:
submission = Submission(**data.model_dump())
session.add(submission)
session.commit()
session.refresh(submission)
return submission
@router.get("/", response_model=list[SubmissionRead])
def list_submissions(
task_id: int | None = None,
team_id: int | None = None,
session: Session = Depends(get_session),
) -> list[Submission]:
query = select(Submission)
if task_id is not None:
query = query.where(Submission.task_id == task_id)
if team_id is not None:
query = query.where(Submission.team_id == team_id)
return list(session.exec(query).all())
@router.get("/{submission_id}", response_model=SubmissionReadWithDetails)
def get_submission(submission_id: int, session: Session = Depends(get_session)) -> Submission:
"""Возвращает работу с вложенными данными команды и задачи."""
submission = session.get(Submission, submission_id)
if not submission:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Работа не найдена")
return submission
@router.delete("/{submission_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_submission(
submission_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
) -> None:
submission = session.get(Submission, submission_id)
if not submission:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Работа не найдена")
session.delete(submission)
session.commit()

73
app/routers/tasks.py Normal file
View File

@@ -0,0 +1,73 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.connection import get_session
from app.models.task import Task
from app.models.user import User
from app.schemas.task import TaskCreate, TaskRead, TaskUpdate
from app.auth.dependencies import get_current_organizer
router = APIRouter(prefix="/tasks", tags=["Tasks"])
@router.post("/", response_model=TaskRead, status_code=status.HTTP_201_CREATED)
def create_task(
data: TaskCreate,
session: Session = Depends(get_session),
organizer: User = Depends(get_current_organizer),
) -> Task:
task = Task(**data.model_dump())
session.add(task)
session.commit()
session.refresh(task)
return task
@router.get("/", response_model=list[TaskRead])
def list_tasks(
hackathon_id: int | None = None,
session: Session = Depends(get_session),
) -> list[Task]:
query = select(Task)
if hackathon_id is not None:
query = query.where(Task.hackathon_id == hackathon_id)
return list(session.exec(query).all())
@router.get("/{task_id}", response_model=TaskRead)
def get_task(task_id: int, session: Session = Depends(get_session)) -> Task:
task = session.get(Task, task_id)
if not task:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Задача не найдена")
return task
@router.patch("/{task_id}", response_model=TaskRead)
def update_task(
task_id: int,
data: TaskUpdate,
session: Session = Depends(get_session),
organizer: User = Depends(get_current_organizer),
) -> Task:
task = session.get(Task, task_id)
if not task:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Задача не найдена")
for field, value in data.model_dump(exclude_unset=True).items():
setattr(task, field, value)
session.add(task)
session.commit()
session.refresh(task)
return task
@router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_task(
task_id: int,
session: Session = Depends(get_session),
organizer: User = Depends(get_current_organizer),
) -> None:
task = session.get(Task, task_id)
if not task:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Задача не найдена")
session.delete(task)
session.commit()

118
app/routers/teams.py Normal file
View File

@@ -0,0 +1,118 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.connection import get_session
from app.models.team import Team
from app.models.team_member import TeamMember
from app.models.user import User
from app.schemas.team import (
TeamCreate,
TeamRead,
TeamReadWithMembers,
TeamMemberAdd,
TeamMemberRead,
)
from app.auth.dependencies import get_current_user
router = APIRouter(prefix="/teams", tags=["Teams"])
@router.post("/", response_model=TeamRead, status_code=status.HTTP_201_CREATED)
def create_team(
data: TeamCreate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
) -> Team:
team = Team(**data.model_dump())
session.add(team)
session.commit()
session.refresh(team)
# Создатель автоматически становится участником команды
member = TeamMember(user_id=current_user.id, team_id=team.id, role="captain")
session.add(member)
session.commit()
session.refresh(team)
return team
@router.get("/", response_model=list[TeamRead])
def list_teams(
hackathon_id: int | None = None,
session: Session = Depends(get_session),
) -> list[Team]:
query = select(Team)
if hackathon_id is not None:
query = query.where(Team.hackathon_id == hackathon_id)
return list(session.exec(query).all())
@router.get("/{team_id}", response_model=TeamReadWithMembers)
def get_team(team_id: int, session: Session = Depends(get_session)) -> Team:
"""Возвращает команду с вложенным списком участников (one-to-many)."""
team = session.get(Team, team_id)
if not team:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Команда не найдена")
return team
@router.delete("/{team_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_team(
team_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
) -> None:
team = session.get(Team, team_id)
if not team:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Команда не найдена")
session.delete(team)
session.commit()
# ── Участники команды ───────────────────────────────────────────────────────
@router.post("/{team_id}/members", response_model=TeamMemberRead, status_code=status.HTTP_201_CREATED)
def add_member(
team_id: int,
data: TeamMemberAdd,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
) -> TeamMember:
team = session.get(Team, team_id)
if not team:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Команда не найдена")
existing = session.exec(
select(TeamMember).where(
TeamMember.user_id == data.user_id,
TeamMember.team_id == team_id,
)
).first()
if existing:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Пользователь уже в команде")
member = TeamMember(user_id=data.user_id, team_id=team_id, role=data.role)
session.add(member)
session.commit()
session.refresh(member)
return member
@router.delete("/{team_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
def remove_member(
team_id: int,
user_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
) -> None:
member = session.exec(
select(TeamMember).where(
TeamMember.user_id == user_id,
TeamMember.team_id == team_id,
)
).first()
if not member:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Участник не найден")
session.delete(member)
session.commit()

66
app/routers/users.py Normal file
View File

@@ -0,0 +1,66 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select
from app.connection import get_session
from app.models.user import User
from app.schemas.user import UserRead, UserUpdate, PasswordChange
from app.auth.dependencies import get_current_user
from app.auth.utils import verify_password, hash_password
router = APIRouter(prefix="/users", tags=["Users"])
@router.get("/me", response_model=UserRead)
def get_me(current_user: User = Depends(get_current_user)) -> User:
return current_user
@router.get("/", response_model=list[UserRead])
def list_users(
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
) -> list[User]:
return list(session.exec(select(User)).all())
@router.get("/{user_id}", response_model=UserRead)
def get_user(
user_id: int,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
) -> User:
user = session.get(User, user_id)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден")
return user
@router.patch("/me", response_model=UserRead)
def update_me(
data: UserUpdate,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
) -> User:
for field, value in data.model_dump(exclude_unset=True).items():
setattr(current_user, field, value)
session.add(current_user)
session.commit()
session.refresh(current_user)
return current_user
@router.post("/me/change-password", status_code=status.HTTP_200_OK)
def change_password(
data: PasswordChange,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_user),
) -> dict:
if not verify_password(data.old_password, current_user.hashed_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Неверный старый пароль",
)
current_user.hashed_password = hash_password(data.new_password)
session.add(current_user)
session.commit()
return {"detail": "Пароль успешно изменён"}

0
app/schemas/__init__.py Normal file
View File

41
app/schemas/hackathon.py Normal file
View File

@@ -0,0 +1,41 @@
from datetime import datetime
from pydantic import BaseModel
from app.schemas.user import UserRead
class HackathonCreate(BaseModel):
title: str
description: str
start_date: datetime
end_date: datetime
class HackathonRead(BaseModel):
id: int
title: str
description: str
start_date: datetime
end_date: datetime
organizer_id: int
class HackathonReadWithOrganizer(HackathonRead):
organizer: UserRead
class ParticipantRegistration(BaseModel):
hackathon_id: int
class ParticipantRead(BaseModel):
id: int
user_id: int
hackathon_id: int
is_confirmed: bool
user: UserRead
class ParticipantConfirm(BaseModel):
user_id: int

27
app/schemas/submission.py Normal file
View File

@@ -0,0 +1,27 @@
from datetime import datetime
from pydantic import BaseModel
from app.schemas.team import TeamRead
from app.schemas.task import TaskRead
class SubmissionCreate(BaseModel):
team_id: int
task_id: int
description: str
file_url: str | None = None
class SubmissionRead(BaseModel):
id: int
team_id: int
task_id: int
description: str
file_url: str | None
submitted_at: datetime
class SubmissionReadWithDetails(SubmissionRead):
team: TeamRead
task: TaskRead

25
app/schemas/task.py Normal file
View File

@@ -0,0 +1,25 @@
from pydantic import BaseModel
class TaskCreate(BaseModel):
hackathon_id: int
title: str
description: str
requirements: str
evaluation_criteria: str
class TaskRead(BaseModel):
id: int
hackathon_id: int
title: str
description: str
requirements: str
evaluation_criteria: str
class TaskUpdate(BaseModel):
title: str | None = None
description: str | None = None
requirements: str | None = None
evaluation_criteria: str | None = None

31
app/schemas/team.py Normal file
View File

@@ -0,0 +1,31 @@
from pydantic import BaseModel
from app.schemas.user import UserRead
class TeamCreate(BaseModel):
name: str
hackathon_id: int
class TeamRead(BaseModel):
id: int
name: str
hackathon_id: int
class TeamMemberAdd(BaseModel):
user_id: int
role: str = "participant"
class TeamMemberRead(BaseModel):
id: int
user_id: int
team_id: int
role: str
user: UserRead
class TeamReadWithMembers(TeamRead):
members: list[TeamMemberRead]

37
app/schemas/user.py Normal file
View File

@@ -0,0 +1,37 @@
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
name: str
email: str
phone: str | None = None
password: str
is_organizer: bool = False
class UserRead(BaseModel):
id: int
name: str
email: str
phone: str | None
is_organizer: bool
class UserUpdate(BaseModel):
name: str | None = None
phone: str | None = None
class PasswordChange(BaseModel):
old_password: str
new_password: str
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
class LoginRequest(BaseModel):
email: str
password: str