init
This commit is contained in:
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
0
app/auth/__init__.py
Normal file
0
app/auth/__init__.py
Normal file
49
app/auth/dependencies.py
Normal file
49
app/auth/dependencies.py
Normal 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
44
app/auth/router.py
Normal 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
29
app/auth/utils.py
Normal 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
13
app/config.py
Normal 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
14
app/connection.py
Normal 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
24
app/main.py
Normal 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
17
app/models/__init__.py
Normal 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
29
app/models/hackathon.py
Normal 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"},
|
||||
)
|
||||
16
app/models/hackathon_participant.py
Normal file
16
app/models/hackathon_participant.py
Normal 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
18
app/models/submission.py
Normal 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
19
app/models/task.py
Normal 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
20
app/models/team.py
Normal 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
16
app/models/team_member.py
Normal 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
26
app/models/user.py
Normal 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
0
app/routers/__init__.py
Normal file
127
app/routers/hackathons.py
Normal file
127
app/routers/hackathons.py
Normal 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
|
||||
59
app/routers/submissions.py
Normal file
59
app/routers/submissions.py
Normal 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
73
app/routers/tasks.py
Normal 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
118
app/routers/teams.py
Normal 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
66
app/routers/users.py
Normal 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
0
app/schemas/__init__.py
Normal file
41
app/schemas/hackathon.py
Normal file
41
app/schemas/hackathon.py
Normal 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
27
app/schemas/submission.py
Normal 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
25
app/schemas/task.py
Normal 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
31
app/schemas/team.py
Normal 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
37
app/schemas/user.py
Normal 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
|
||||
Reference in New Issue
Block a user