Add 3 roles, settings for marathons

This commit is contained in:
2025-12-14 20:21:56 +07:00
parent bb9e9a6e1d
commit d0b8eca600
28 changed files with 1679 additions and 290 deletions

View File

@@ -1,17 +1,21 @@
from app.models.user import User
from app.models.marathon import Marathon, MarathonStatus
from app.models.participant import Participant
from app.models.game import Game
from app.models.user import User, UserRole
from app.models.marathon import Marathon, MarathonStatus, GameProposalMode
from app.models.participant import Participant, ParticipantRole
from app.models.game import Game, GameStatus
from app.models.challenge import Challenge, ChallengeType, Difficulty, ProofType
from app.models.assignment import Assignment, AssignmentStatus
from app.models.activity import Activity, ActivityType
__all__ = [
"User",
"UserRole",
"Marathon",
"MarathonStatus",
"GameProposalMode",
"Participant",
"ParticipantRole",
"Game",
"GameStatus",
"Challenge",
"ChallengeType",
"Difficulty",

View File

@@ -13,6 +13,9 @@ class ActivityType(str, Enum):
DROP = "drop"
START_MARATHON = "start_marathon"
FINISH_MARATHON = "finish_marathon"
ADD_GAME = "add_game"
APPROVE_GAME = "approve_game"
REJECT_GAME = "reject_game"
class Activity(Base):

View File

@@ -1,10 +1,17 @@
from datetime import datetime
from enum import Enum
from sqlalchemy import String, DateTime, ForeignKey, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class GameStatus(str, Enum):
PENDING = "pending" # Предложена участником, ждёт модерации
APPROVED = "approved" # Одобрена организатором
REJECTED = "rejected" # Отклонена
class Game(Base):
__tablename__ = "games"
@@ -14,14 +21,33 @@ class Game(Base):
cover_path: Mapped[str | None] = mapped_column(String(500), nullable=True)
download_url: Mapped[str] = mapped_column(Text, nullable=False)
genre: Mapped[str | None] = mapped_column(String(50), nullable=True)
added_by_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
status: Mapped[str] = mapped_column(String(20), default=GameStatus.PENDING.value)
proposed_by_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
approved_by_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
# Relationships
marathon: Mapped["Marathon"] = relationship("Marathon", back_populates="games")
added_by_user: Mapped["User"] = relationship("User", back_populates="added_games")
proposed_by: Mapped["User"] = relationship(
"User",
back_populates="proposed_games",
foreign_keys=[proposed_by_id]
)
approved_by: Mapped["User | None"] = relationship(
"User",
back_populates="approved_games",
foreign_keys=[approved_by_id]
)
challenges: Mapped[list["Challenge"]] = relationship(
"Challenge",
back_populates="game",
cascade="all, delete-orphan"
)
@property
def is_approved(self) -> bool:
return self.status == GameStatus.APPROVED.value
@property
def is_pending(self) -> bool:
return self.status == GameStatus.PENDING.value

View File

@@ -1,6 +1,6 @@
from datetime import datetime
from enum import Enum
from sqlalchemy import String, Text, DateTime, ForeignKey, Integer
from sqlalchemy import String, Text, DateTime, ForeignKey, Integer, Boolean
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
@@ -12,24 +12,31 @@ class MarathonStatus(str, Enum):
FINISHED = "finished"
class GameProposalMode(str, Enum):
ALL_PARTICIPANTS = "all_participants"
ORGANIZER_ONLY = "organizer_only"
class Marathon(Base):
__tablename__ = "marathons"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(100), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
organizer_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
creator_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
status: Mapped[str] = mapped_column(String(20), default=MarathonStatus.PREPARING.value)
invite_code: Mapped[str] = mapped_column(String(20), unique=True, nullable=False, index=True)
is_public: Mapped[bool] = mapped_column(Boolean, default=False)
game_proposal_mode: Mapped[str] = mapped_column(String(20), default=GameProposalMode.ALL_PARTICIPANTS.value)
start_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
end_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
# Relationships
organizer: Mapped["User"] = relationship(
creator: Mapped["User"] = relationship(
"User",
back_populates="organized_marathons",
foreign_keys=[organizer_id]
back_populates="created_marathons",
foreign_keys=[creator_id]
)
participants: Mapped[list["Participant"]] = relationship(
"Participant",

View File

@@ -1,10 +1,16 @@
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, UniqueConstraint
from enum import Enum
from sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class ParticipantRole(str, Enum):
PARTICIPANT = "participant"
ORGANIZER = "organizer"
class Participant(Base):
__tablename__ = "participants"
__table_args__ = (
@@ -14,6 +20,7 @@ class Participant(Base):
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True)
marathon_id: Mapped[int] = mapped_column(ForeignKey("marathons.id", ondelete="CASCADE"), index=True)
role: Mapped[str] = mapped_column(String(20), default=ParticipantRole.PARTICIPANT.value)
total_points: Mapped[int] = mapped_column(Integer, default=0)
current_streak: Mapped[int] = mapped_column(Integer, default=0)
drop_count: Mapped[int] = mapped_column(Integer, default=0) # For progressive penalty
@@ -27,3 +34,7 @@ class Participant(Base):
back_populates="participant",
cascade="all, delete-orphan"
)
@property
def is_organizer(self) -> bool:
return self.role == ParticipantRole.ORGANIZER.value

View File

@@ -1,10 +1,16 @@
from datetime import datetime
from enum import Enum
from sqlalchemy import String, BigInteger, DateTime
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
class UserRole(str, Enum):
USER = "user"
ADMIN = "admin"
class User(Base):
__tablename__ = "users"
@@ -15,19 +21,36 @@ class User(Base):
avatar_path: Mapped[str | None] = mapped_column(String(500), nullable=True)
telegram_id: Mapped[int | None] = mapped_column(BigInteger, unique=True, nullable=True)
telegram_username: Mapped[str | None] = mapped_column(String(50), nullable=True)
role: Mapped[str] = mapped_column(String(20), default=UserRole.USER.value)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
# Relationships
organized_marathons: Mapped[list["Marathon"]] = relationship(
created_marathons: Mapped[list["Marathon"]] = relationship(
"Marathon",
back_populates="organizer",
foreign_keys="Marathon.organizer_id"
back_populates="creator",
foreign_keys="Marathon.creator_id"
)
participations: Mapped[list["Participant"]] = relationship(
"Participant",
back_populates="user"
)
added_games: Mapped[list["Game"]] = relationship(
proposed_games: Mapped[list["Game"]] = relationship(
"Game",
back_populates="added_by_user"
back_populates="proposed_by",
foreign_keys="Game.proposed_by_id"
)
approved_games: Mapped[list["Game"]] = relationship(
"Game",
back_populates="approved_by",
foreign_keys="Game.approved_by_id"
)
@property
def is_admin(self) -> bool:
return self.role == UserRole.ADMIN.value
@property
def avatar_url(self) -> str | None:
if self.avatar_path:
return f"/uploads/avatars/{self.avatar_path.split('/')[-1]}"
return None