Add backend stuff
							parent
							
								
									67567046b8
								
							
						
					
					
						commit
						4d76cdce44
					
				| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					# Use an official Python runtime as a parent image
 | 
				
			||||||
 | 
					FROM python:3.11-slim
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Set the working directory in the container
 | 
				
			||||||
 | 
					WORKDIR /app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Install dependencies
 | 
				
			||||||
 | 
					COPY requirements.txt ./
 | 
				
			||||||
 | 
					RUN pip install --no-cache-dir -r requirements.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Copy the entire app into the container
 | 
				
			||||||
 | 
					COPY . .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Expose the Flask development server port
 | 
				
			||||||
 | 
					EXPOSE 5000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Set the Flask environment to development
 | 
				
			||||||
 | 
					ENV FLASK_APP=app.py
 | 
				
			||||||
 | 
					ENV FLASK_ENV=development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Command to run the Flask application
 | 
				
			||||||
 | 
					CMD ["flask", "run", "--host=0.0.0.0", "--port=5000"]
 | 
				
			||||||
| 
						 | 
					@ -8,9 +8,12 @@ from models import init_db
 | 
				
			||||||
from spec import spec
 | 
					from spec import spec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app = Flask(__name__)
 | 
					app = Flask(__name__)
 | 
				
			||||||
CORS(app, origins=["http://localhost:5173"], supports_credentials=True)
 | 
					#CORS(app, origins=["http://localhost:5173"], supports_credentials=True)
 | 
				
			||||||
CORS(login.api_login, origins=["http://localhost:5173"], supports_credentials=True)
 | 
					#CORS(login.api_login, origins=["http://localhost:5173"], supports_credentials=True)
 | 
				
			||||||
CORS(schedule.api_schedule, origins=["http://localhost:5173"], supports_credentials=True)
 | 
					#CORS(schedule.api_schedule, origins=["http://localhost:5173"], supports_credentials=True)
 | 
				
			||||||
 | 
					CORS(app, origins=["*"], supports_credentials=True)
 | 
				
			||||||
 | 
					CORS(login.api_login, origins=["*"], supports_credentials=True)
 | 
				
			||||||
 | 
					CORS(schedule.api_schedule, origins=["*"], supports_credentials=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
init_db(app)
 | 
					init_db(app)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,55 @@
 | 
				
			||||||
 | 
					"""Add Team.tz_timezone
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Revision ID: ea359b0e46d7
 | 
				
			||||||
 | 
					Revises: 2b2f3ae2ec7f
 | 
				
			||||||
 | 
					Create Date: 2024-11-03 16:53:37.904012
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from alembic import op
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					import sqlalchemy_utc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# revision identifiers, used by Alembic.
 | 
				
			||||||
 | 
					revision = 'ea359b0e46d7'
 | 
				
			||||||
 | 
					down_revision = '2b2f3ae2ec7f'
 | 
				
			||||||
 | 
					branch_labels = None
 | 
				
			||||||
 | 
					depends_on = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def upgrade():
 | 
				
			||||||
 | 
					    # ### commands auto generated by Alembic - please adjust! ###
 | 
				
			||||||
 | 
					    with op.batch_alter_table('players_teams_availability', schema=None) as batch_op:
 | 
				
			||||||
 | 
					        batch_op.alter_column('start_time',
 | 
				
			||||||
 | 
					               existing_type=sa.TIMESTAMP(),
 | 
				
			||||||
 | 
					               type_=sqlalchemy_utc.sqltypes.UtcDateTime(timezone=True),
 | 
				
			||||||
 | 
					               existing_nullable=False)
 | 
				
			||||||
 | 
					        batch_op.alter_column('end_time',
 | 
				
			||||||
 | 
					               existing_type=sa.TIMESTAMP(),
 | 
				
			||||||
 | 
					               type_=sqlalchemy_utc.sqltypes.UtcDateTime(timezone=True),
 | 
				
			||||||
 | 
					               existing_nullable=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with op.batch_alter_table('teams', schema=None) as batch_op:
 | 
				
			||||||
 | 
					        batch_op.add_column(sa.Column('tz_timezone', sa.String(length=31), nullable=False, default='Etc/UTC', server_default='0'))
 | 
				
			||||||
 | 
					        batch_op.add_column(sa.Column('minute_offset', sa.SmallInteger(), nullable=False, default=0, server_default='0'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ### end Alembic commands ###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def downgrade():
 | 
				
			||||||
 | 
					    # ### commands auto generated by Alembic - please adjust! ###
 | 
				
			||||||
 | 
					    with op.batch_alter_table('teams', schema=None) as batch_op:
 | 
				
			||||||
 | 
					        batch_op.drop_column('minute_offset')
 | 
				
			||||||
 | 
					        batch_op.drop_column('tz_timezone')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with op.batch_alter_table('players_teams_availability', schema=None) as batch_op:
 | 
				
			||||||
 | 
					        batch_op.alter_column('end_time',
 | 
				
			||||||
 | 
					               existing_type=sqlalchemy_utc.sqltypes.UtcDateTime(timezone=True),
 | 
				
			||||||
 | 
					               type_=sa.TIMESTAMP(),
 | 
				
			||||||
 | 
					               existing_nullable=False)
 | 
				
			||||||
 | 
					        batch_op.alter_column('start_time',
 | 
				
			||||||
 | 
					               existing_type=sqlalchemy_utc.sqltypes.UtcDateTime(timezone=True),
 | 
				
			||||||
 | 
					               type_=sa.TIMESTAMP(),
 | 
				
			||||||
 | 
					               existing_nullable=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ### end Alembic commands ###
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					"""Add PlayerTeam.is_team_leader
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Revision ID: f50a79c4ae22
 | 
				
			||||||
 | 
					Revises: ea359b0e46d7
 | 
				
			||||||
 | 
					Create Date: 2024-11-03 17:11:35.956743
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from alembic import op
 | 
				
			||||||
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# revision identifiers, used by Alembic.
 | 
				
			||||||
 | 
					revision = 'f50a79c4ae22'
 | 
				
			||||||
 | 
					down_revision = 'ea359b0e46d7'
 | 
				
			||||||
 | 
					branch_labels = None
 | 
				
			||||||
 | 
					depends_on = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def upgrade():
 | 
				
			||||||
 | 
					    # ### commands auto generated by Alembic - please adjust! ###
 | 
				
			||||||
 | 
					    with op.batch_alter_table('players_teams', schema=None) as batch_op:
 | 
				
			||||||
 | 
					        batch_op.add_column(sa.Column('is_team_leader', sa.Boolean(), nullable=False, server_default='0'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ### end Alembic commands ###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def downgrade():
 | 
				
			||||||
 | 
					    # ### commands auto generated by Alembic - please adjust! ###
 | 
				
			||||||
 | 
					    with op.batch_alter_table('players_teams', schema=None) as batch_op:
 | 
				
			||||||
 | 
					        batch_op.drop_column('is_team_leader')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ### end Alembic commands ###
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ from typing import List
 | 
				
			||||||
from flask import Flask
 | 
					from flask import Flask
 | 
				
			||||||
from flask_sqlalchemy import SQLAlchemy
 | 
					from flask_sqlalchemy import SQLAlchemy
 | 
				
			||||||
from flask_migrate import Migrate
 | 
					from flask_migrate import Migrate
 | 
				
			||||||
from sqlalchemy import TIMESTAMP, BigInteger, Boolean, Enum, ForeignKey, ForeignKeyConstraint, Integer, Interval, MetaData, String, func
 | 
					from sqlalchemy import TIMESTAMP, BigInteger, Boolean, Enum, ForeignKey, ForeignKeyConstraint, Integer, Interval, MetaData, SmallInteger, String, func
 | 
				
			||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
 | 
					from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
 | 
				
			||||||
from sqlalchemy_utc import UtcDateTime
 | 
					from sqlalchemy_utc import UtcDateTime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,10 +36,9 @@ class Player(db.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
 | 
					    created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PlayerSpec(spec.BaseModel):
 | 
					class PlayerSchema(spec.BaseModel):
 | 
				
			||||||
    steam_id: str
 | 
					    steam_id: str
 | 
				
			||||||
    username: str
 | 
					    username: str
 | 
				
			||||||
    #teams: list["PlayerTeamSpec"]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Team(db.Model):
 | 
					class Team(db.Model):
 | 
				
			||||||
    __tablename__ = "teams"
 | 
					    __tablename__ = "teams"
 | 
				
			||||||
| 
						 | 
					@ -47,12 +46,14 @@ class Team(db.Model):
 | 
				
			||||||
    id: Mapped[int] = mapped_column(Integer, autoincrement=True, primary_key=True)
 | 
					    id: Mapped[int] = mapped_column(Integer, autoincrement=True, primary_key=True)
 | 
				
			||||||
    team_name: Mapped[str] = mapped_column(String(63), unique=True)
 | 
					    team_name: Mapped[str] = mapped_column(String(63), unique=True)
 | 
				
			||||||
    discord_webhook_url: Mapped[str] = mapped_column(String(255), nullable=True)
 | 
					    discord_webhook_url: Mapped[str] = mapped_column(String(255), nullable=True)
 | 
				
			||||||
 | 
					    tz_timezone: Mapped[str] = mapped_column(String(31), default="Etc/UTC")
 | 
				
			||||||
 | 
					    minute_offset: Mapped[int] = mapped_column(SmallInteger, default=0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    players: Mapped[List["PlayerTeam"]] = relationship(back_populates="team")
 | 
					    players: Mapped[List["PlayerTeam"]] = relationship(back_populates="team")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
 | 
					    created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TeamSpec(spec.BaseModel):
 | 
					class TeamSchema(spec.BaseModel):
 | 
				
			||||||
    id: int
 | 
					    id: int
 | 
				
			||||||
    team_name: str
 | 
					    team_name: str
 | 
				
			||||||
    discord_webhook_url: str | None
 | 
					    discord_webhook_url: str | None
 | 
				
			||||||
| 
						 | 
					@ -71,13 +72,18 @@ class PlayerTeam(db.Model):
 | 
				
			||||||
    player: Mapped["Player"] = relationship(back_populates="teams")
 | 
					    player: Mapped["Player"] = relationship(back_populates="teams")
 | 
				
			||||||
    team: Mapped["Team"] = relationship(back_populates="players")
 | 
					    team: Mapped["Team"] = relationship(back_populates="players")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    player_roles: Mapped[List["PlayerTeamRole"]] = relationship(back_populates="player_team")
 | 
					    player_roles: Mapped[List["PlayerTeamRole"]] = relationship("PlayerTeamRole", back_populates="player_team")
 | 
				
			||||||
    availability: Mapped[List["PlayerTeamAvailability"]] = relationship(back_populates="player_team")
 | 
					    availability: Mapped[List["PlayerTeamAvailability"]] = relationship(back_populates="player_team")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    team_role: Mapped[TeamRole] = mapped_column(Enum(TeamRole), default=TeamRole.Player)
 | 
					    team_role: Mapped[TeamRole] = mapped_column(Enum(TeamRole), default=TeamRole.Player)
 | 
				
			||||||
    playtime: Mapped[timedelta] = mapped_column(Interval)
 | 
					    playtime: Mapped[timedelta] = mapped_column(Interval, default=timedelta(0))
 | 
				
			||||||
 | 
					    is_team_leader: Mapped[bool] = mapped_column(Boolean, default=False)
 | 
				
			||||||
    created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
 | 
					    created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PlayerTeamSchema(spec.BaseModel):
 | 
				
			||||||
 | 
					    player: PlayerSchema
 | 
				
			||||||
 | 
					    team: TeamSchema
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PlayerTeamRole(db.Model):
 | 
					class PlayerTeamRole(db.Model):
 | 
				
			||||||
    __tablename__ = "players_teams_roles"
 | 
					    __tablename__ = "players_teams_roles"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -107,7 +113,7 @@ class PlayerTeamRole(db.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #player: Mapped["Player"] = relationship(back_populates="teams")
 | 
					    #player: Mapped["Player"] = relationship(back_populates="teams")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    role: Mapped[Role] = mapped_column(Enum(Role))
 | 
					    role: Mapped[Role] = mapped_column(Enum(Role), primary_key=True)
 | 
				
			||||||
    is_main: Mapped[bool] = mapped_column(Boolean)
 | 
					    is_main: Mapped[bool] = mapped_column(Boolean)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    __table_args__ = (
 | 
					    __table_args__ = (
 | 
				
			||||||
| 
						 | 
					@ -125,7 +131,7 @@ class PlayerTeamAvailability(db.Model):
 | 
				
			||||||
    start_time: Mapped[datetime] = mapped_column(UtcDateTime, primary_key=True)
 | 
					    start_time: Mapped[datetime] = mapped_column(UtcDateTime, primary_key=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    player_team: Mapped["PlayerTeam"] = relationship(
 | 
					    player_team: Mapped["PlayerTeam"] = relationship(
 | 
				
			||||||
            "PlayerTeam",back_populates="availability")
 | 
					            "PlayerTeam", back_populates="availability")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    availability: Mapped[int] = mapped_column(Integer, default=2)
 | 
					    availability: Mapped[int] = mapped_column(Integer, default=2)
 | 
				
			||||||
    end_time: Mapped[datetime] = mapped_column(UtcDateTime)
 | 
					    end_time: Mapped[datetime] = mapped_column(UtcDateTime)
 | 
				
			||||||
| 
						 | 
					@ -154,6 +160,7 @@ class AuthSession(db.Model):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def init_db(app: Flask):
 | 
					def init_db(app: Flask):
 | 
				
			||||||
    app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///db.sqlite3"
 | 
					    app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///db.sqlite3"
 | 
				
			||||||
 | 
					    #app.config["SQLALCHEMY_ECHO"] = True
 | 
				
			||||||
    db.init_app(app)
 | 
					    db.init_app(app)
 | 
				
			||||||
    migrate.init_app(app, db)
 | 
					    migrate.init_app(app, db)
 | 
				
			||||||
    return app
 | 
					    return app
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,3 +18,5 @@ alembic
 | 
				
			||||||
Flask-Migrate
 | 
					Flask-Migrate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
requests
 | 
					requests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pytz  # timezone handling
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@ import datetime
 | 
				
			||||||
from typing import cast
 | 
					from typing import cast
 | 
				
			||||||
from flask import Blueprint, abort, jsonify, make_response, request
 | 
					from flask import Blueprint, abort, jsonify, make_response, request
 | 
				
			||||||
from flask_pydantic import validate
 | 
					from flask_pydantic import validate
 | 
				
			||||||
 | 
					from spectree import Response
 | 
				
			||||||
from models import Player, PlayerTeam, PlayerTeamAvailability, PlayerTeamRole, db
 | 
					from models import Player, PlayerTeam, PlayerTeamAvailability, PlayerTeamRole, db
 | 
				
			||||||
from middleware import requires_authentication
 | 
					from middleware import requires_authentication
 | 
				
			||||||
from spec import spec, BaseModel
 | 
					from spec import spec, BaseModel
 | 
				
			||||||
| 
						 | 
					@ -14,8 +15,15 @@ class ViewScheduleForm(BaseModel):
 | 
				
			||||||
    team_id: int
 | 
					    team_id: int
 | 
				
			||||||
    window_size_days: int = 7
 | 
					    window_size_days: int = 7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ViewScheduleResponse(BaseModel):
 | 
				
			||||||
 | 
					    availability: list[int]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_schedule.get("/")
 | 
					@api_schedule.get("/")
 | 
				
			||||||
@spec.validate()
 | 
					@spec.validate(
 | 
				
			||||||
 | 
					    resp=Response(
 | 
				
			||||||
 | 
					        HTTP_200=ViewScheduleResponse
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@requires_authentication
 | 
					@requires_authentication
 | 
				
			||||||
def get(query: ViewScheduleForm, player: Player, **kwargs):
 | 
					def get(query: ViewScheduleForm, player: Player, **kwargs):
 | 
				
			||||||
    window_start = query.window_start
 | 
					    window_start = query.window_start
 | 
				
			||||||
| 
						 | 
					@ -174,7 +182,7 @@ def put(json: PutScheduleForm, player: Player, **kwargs):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db.session.add_all(availability_blocks)
 | 
					    db.session.add_all(availability_blocks)
 | 
				
			||||||
    db.session.commit()
 | 
					    db.session.commit()
 | 
				
			||||||
    return make_response({ }, 300)
 | 
					    return make_response({ }, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ViewAvailablePlayersForm(BaseModel):
 | 
					class ViewAvailablePlayersForm(BaseModel):
 | 
				
			||||||
    start_time: datetime.datetime
 | 
					    start_time: datetime.datetime
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,27 @@
 | 
				
			||||||
 | 
					import pydantic.v1
 | 
				
			||||||
 | 
					from spectree import SpecTree
 | 
				
			||||||
 | 
					from pydantic.alias_generators import to_camel
 | 
				
			||||||
 | 
					from spectree.plugins.flask_plugin import FlaskPlugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Naming convention:
 | 
				
			||||||
 | 
					# https://github.com/0b01001001/spectree/issues/300
 | 
				
			||||||
 | 
					# https://github.com/0b01001001/spectree/pull/302
 | 
				
			||||||
 | 
					def naming_strategy(model):
 | 
				
			||||||
 | 
					    return model.__name__
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# https://github.com/0b01001001/spectree/issues/304#issuecomment-1519961668
 | 
				
			||||||
 | 
					def nested_naming_strategy(_, child):
 | 
				
			||||||
 | 
					    return child
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					spec = SpecTree(
 | 
				
			||||||
 | 
					    "flask",
 | 
				
			||||||
 | 
					    annotations=True,
 | 
				
			||||||
 | 
					    naming_strategy=naming_strategy,
 | 
				
			||||||
 | 
					    nested_naming_strategy=nested_naming_strategy
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BaseModel(pydantic.v1.BaseModel):
 | 
				
			||||||
 | 
					    class Config:
 | 
				
			||||||
 | 
					        alias_generator = to_camel
 | 
				
			||||||
 | 
					        allow_population_by_field_name = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,27 +1,174 @@
 | 
				
			||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
from typing import List
 | 
					from typing import List
 | 
				
			||||||
from flask import Blueprint, abort, jsonify, make_response, request
 | 
					from flask import Blueprint, abort, jsonify, make_response, request
 | 
				
			||||||
import pydantic
 | 
					import pydantic
 | 
				
			||||||
 | 
					from pydantic.v1 import validator
 | 
				
			||||||
from flask_pydantic import validate
 | 
					from flask_pydantic import validate
 | 
				
			||||||
from spectree import Response
 | 
					from spectree import Response
 | 
				
			||||||
from models import Player, PlayerTeam, Team, TeamSpec, db
 | 
					from sqlalchemy.orm import joinedload, subqueryload
 | 
				
			||||||
 | 
					from models import Player, PlayerSchema, PlayerTeam, PlayerTeamAvailability, PlayerTeamRole, PlayerTeamSchema, Team, TeamSchema, db
 | 
				
			||||||
from middleware import requires_authentication
 | 
					from middleware import requires_authentication
 | 
				
			||||||
import models
 | 
					import models
 | 
				
			||||||
from spec import spec, BaseModel
 | 
					from spec import spec, BaseModel
 | 
				
			||||||
 | 
					import pytz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
api_team = Blueprint("team", __name__, url_prefix="/team")
 | 
					api_team = Blueprint("team", __name__, url_prefix="/team")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def map_team_to_schema(team: Team):
 | 
				
			||||||
 | 
					    return TeamSchema(
 | 
				
			||||||
 | 
					        id=team.id,
 | 
				
			||||||
 | 
					        team_name=team.team_name,
 | 
				
			||||||
 | 
					        discord_webhook_url=None
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def map_player_to_schema(player: Player):
 | 
				
			||||||
 | 
					    return PlayerSchema(
 | 
				
			||||||
 | 
					        steam_id=str(player.steam_id),
 | 
				
			||||||
 | 
					        username=player.username,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CreateTeamJson(BaseModel):
 | 
					class CreateTeamJson(BaseModel):
 | 
				
			||||||
    team_name: str
 | 
					    team_name: str
 | 
				
			||||||
    webhook_url: str
 | 
					    discord_webhook_url: str | None = None
 | 
				
			||||||
    timezone: str
 | 
					    league_timezone: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @validator("league_timezone")
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def validate_timezone(cls, v):
 | 
				
			||||||
 | 
					        if v not in pytz.all_timezones:
 | 
				
			||||||
 | 
					            raise ValueError(v + " is not a valid timezone")
 | 
				
			||||||
 | 
					        return v
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @validator("team_name")
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def validate_team_name(cls, v: str):
 | 
				
			||||||
 | 
					        if not v:
 | 
				
			||||||
 | 
					            raise ValueError("Team name can not be blank")
 | 
				
			||||||
 | 
					        return v
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ViewTeamResponse(BaseModel):
 | 
					class ViewTeamResponse(BaseModel):
 | 
				
			||||||
    team: models.TeamSpec
 | 
					    team: models.TeamSchema
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ViewTeamsResponse(BaseModel):
 | 
					class ViewTeamsResponse(BaseModel):
 | 
				
			||||||
    teams: list[models.TeamSpec]
 | 
					    teams: list[models.TeamSchema]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@api_team.post("/")
 | 
				
			||||||
 | 
					@spec.validate(
 | 
				
			||||||
 | 
					    resp=Response(
 | 
				
			||||||
 | 
					        HTTP_200=ViewTeamResponse,
 | 
				
			||||||
 | 
					        HTTP_403=None,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    operation_id="create_team"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					@requires_authentication
 | 
				
			||||||
 | 
					def create_team(json: CreateTeamJson, player: Player, **kwargs):
 | 
				
			||||||
 | 
					    team = Team(
 | 
				
			||||||
 | 
					        team_name=json.team_name,
 | 
				
			||||||
 | 
					        tz_timezone=json.league_timezone,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    if json.discord_webhook_url:
 | 
				
			||||||
 | 
					        team.discord_webhook_url = json.discord_webhook_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    db.session.add(team)
 | 
				
			||||||
 | 
					    db.session.flush()  # flush, so we can get autoincremented id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    player_team = PlayerTeam(
 | 
				
			||||||
 | 
					        player_id=player.steam_id,
 | 
				
			||||||
 | 
					        team_id=team.id,
 | 
				
			||||||
 | 
					        is_team_leader=True
 | 
				
			||||||
 | 
					     )
 | 
				
			||||||
 | 
					    db.session.add(player_team)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    response = ViewTeamResponse(team=map_team_to_schema(team))
 | 
				
			||||||
 | 
					    return jsonify(response.dict())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@api_team.delete("/id/<team_id>/")
 | 
				
			||||||
 | 
					@spec.validate(
 | 
				
			||||||
 | 
					    resp=Response(
 | 
				
			||||||
 | 
					        HTTP_200=None,
 | 
				
			||||||
 | 
					        HTTP_403=None,
 | 
				
			||||||
 | 
					        HTTP_404=None,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    operation_id="delete_team"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def delete_team(player: Player, team_id: int):
 | 
				
			||||||
 | 
					    player_team = db.session.query(
 | 
				
			||||||
 | 
					        PlayerTeam
 | 
				
			||||||
 | 
					    ).where(
 | 
				
			||||||
 | 
					        PlayerTeam.team_id == team_id
 | 
				
			||||||
 | 
					    ).where(
 | 
				
			||||||
 | 
					        PlayerTeam.player_id == player.steam_id
 | 
				
			||||||
 | 
					    ).one_or_none()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not player_team:
 | 
				
			||||||
 | 
					        abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not player_team.is_team_leader:
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    db.session.delete(player_team.team)
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    return make_response(200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AddPlayerJson(BaseModel):
 | 
				
			||||||
 | 
					    team_role: PlayerTeam.TeamRole = PlayerTeam.TeamRole.Player
 | 
				
			||||||
 | 
					    is_team_leader: bool = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@api_team.put("/id/<team_id>/player/<player_id>/")
 | 
				
			||||||
 | 
					@spec.validate(
 | 
				
			||||||
 | 
					    resp=Response(
 | 
				
			||||||
 | 
					        HTTP_200=None,
 | 
				
			||||||
 | 
					        HTTP_403=None,
 | 
				
			||||||
 | 
					        HTTP_404=None,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    operation_id="create_or_update_player"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def add_player(player: Player, team_id: int, player_id: int, json: AddPlayerJson):
 | 
				
			||||||
 | 
					    player_team = db.session.query(
 | 
				
			||||||
 | 
					        PlayerTeam
 | 
				
			||||||
 | 
					    ).where(
 | 
				
			||||||
 | 
					        PlayerTeam.player_id == player.steam_id
 | 
				
			||||||
 | 
					    ).where(
 | 
				
			||||||
 | 
					        PlayerTeam.team_id == team_id
 | 
				
			||||||
 | 
					    ).one_or_none()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not player_team:
 | 
				
			||||||
 | 
					        abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not player_team.is_team_leader:
 | 
				
			||||||
 | 
					        abort(403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    target_player_team = db.session.query(
 | 
				
			||||||
 | 
					        PlayerTeam
 | 
				
			||||||
 | 
					    ).where(
 | 
				
			||||||
 | 
					        PlayerTeam.player_id == player_id
 | 
				
			||||||
 | 
					    ).where(
 | 
				
			||||||
 | 
					        PlayerTeam.team_id == team_id
 | 
				
			||||||
 | 
					    ).one_or_none()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not target_player_team:
 | 
				
			||||||
 | 
					        target_player = db.session.query(
 | 
				
			||||||
 | 
					            Player
 | 
				
			||||||
 | 
					        ).where(
 | 
				
			||||||
 | 
					            Player.steam_id == player_id
 | 
				
			||||||
 | 
					        ).one_or_none()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not target_player:
 | 
				
			||||||
 | 
					            abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        target_player_team = PlayerTeam()
 | 
				
			||||||
 | 
					        target_player_team.player_id = player_id
 | 
				
			||||||
 | 
					        target_player_team.team_id = player_team.team_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    target_player_team.team_role = json.team_role
 | 
				
			||||||
 | 
					    target_player_team.is_team_leader = json.is_team_leader
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    db.session.commit()
 | 
				
			||||||
 | 
					    return make_response(200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_team.get("/all/")
 | 
					@api_team.get("/all/")
 | 
				
			||||||
@spec.validate(
 | 
					@spec.validate(
 | 
				
			||||||
| 
						 | 
					@ -29,7 +176,8 @@ class ViewTeamsResponse(BaseModel):
 | 
				
			||||||
        HTTP_200=ViewTeamsResponse,
 | 
					        HTTP_200=ViewTeamsResponse,
 | 
				
			||||||
        HTTP_403=None,
 | 
					        HTTP_403=None,
 | 
				
			||||||
        HTTP_404=None,
 | 
					        HTTP_404=None,
 | 
				
			||||||
    )
 | 
					    ),
 | 
				
			||||||
 | 
					    operation_id="get_teams"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@requires_authentication
 | 
					@requires_authentication
 | 
				
			||||||
def view_teams(**kwargs):
 | 
					def view_teams(**kwargs):
 | 
				
			||||||
| 
						 | 
					@ -45,7 +193,8 @@ def view_teams(**kwargs):
 | 
				
			||||||
        HTTP_200=ViewTeamResponse,
 | 
					        HTTP_200=ViewTeamResponse,
 | 
				
			||||||
        HTTP_403=None,
 | 
					        HTTP_403=None,
 | 
				
			||||||
        HTTP_404=None,
 | 
					        HTTP_404=None,
 | 
				
			||||||
    )
 | 
					    ),
 | 
				
			||||||
 | 
					    operation_id="get_team"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@requires_authentication
 | 
					@requires_authentication
 | 
				
			||||||
def view_team(team_id: int, **kwargs):
 | 
					def view_team(team_id: int, **kwargs):
 | 
				
			||||||
| 
						 | 
					@ -69,21 +218,82 @@ def fetch_teams_for_player(player: Player, team_id: int | None):
 | 
				
			||||||
    if team_id is not None:
 | 
					    if team_id is not None:
 | 
				
			||||||
        q = q.where(PlayerTeam.team_id == team_id)
 | 
					        q = q.where(PlayerTeam.team_id == team_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def map_team_to_spec(team: Team) -> TeamSpec:
 | 
					 | 
				
			||||||
        return TeamSpec(
 | 
					 | 
				
			||||||
            id=team.id,
 | 
					 | 
				
			||||||
            team_name=team.team_name,
 | 
					 | 
				
			||||||
            discord_webhook_url=None
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if team_id is None:
 | 
					    if team_id is None:
 | 
				
			||||||
        teams = q.all()
 | 
					        teams = q.all()
 | 
				
			||||||
        return ViewTeamsResponse(
 | 
					        return ViewTeamsResponse(
 | 
				
			||||||
            teams=list(map(map_team_to_spec, teams))
 | 
					            teams=list(map(map_team_to_schema, teams))
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        team = q.one_or_none()
 | 
					        team = q.one_or_none()
 | 
				
			||||||
        if team:
 | 
					        if team:
 | 
				
			||||||
            return ViewTeamResponse(
 | 
					            return ViewTeamResponse(
 | 
				
			||||||
                team=map_team_to_spec(team)
 | 
					                team=map_team_to_schema(team)
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ViewTeamMembersResponse(PlayerSchema):
 | 
				
			||||||
 | 
					    class RoleSchema(BaseModel):
 | 
				
			||||||
 | 
					        role: str
 | 
				
			||||||
 | 
					        is_main: bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    roles: list[RoleSchema]
 | 
				
			||||||
 | 
					    availability: int
 | 
				
			||||||
 | 
					    playtime: float
 | 
				
			||||||
 | 
					    created_at: datetime.datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@api_team.get("/id/<team_id>/players")
 | 
				
			||||||
 | 
					@spec.validate(
 | 
				
			||||||
 | 
					    resp=Response(
 | 
				
			||||||
 | 
					        HTTP_200=list[ViewTeamMembersResponse],
 | 
				
			||||||
 | 
					        HTTP_403=None,
 | 
				
			||||||
 | 
					        HTTP_404=None,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    operation_id="get_team_members"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					@requires_authentication
 | 
				
			||||||
 | 
					def view_team_members(player: Player, team_id: int, **kwargs):
 | 
				
			||||||
 | 
					    now = datetime.datetime.now(datetime.timezone.utc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    player_teams_query = db.session.query(
 | 
				
			||||||
 | 
					        PlayerTeam
 | 
				
			||||||
 | 
					    ).where(
 | 
				
			||||||
 | 
					        PlayerTeam.team_id == team_id
 | 
				
			||||||
 | 
					    ).options(
 | 
				
			||||||
 | 
					        # eager load so SQLAlchemy does not make a second query just to join
 | 
				
			||||||
 | 
					        joinedload(PlayerTeam.player),
 | 
				
			||||||
 | 
					        joinedload(PlayerTeam.player_roles),
 | 
				
			||||||
 | 
					        joinedload(PlayerTeam.availability.and_(
 | 
				
			||||||
 | 
					            (PlayerTeamAvailability.start_time <= now) &
 | 
				
			||||||
 | 
					                (PlayerTeamAvailability.end_time > now)
 | 
				
			||||||
 | 
					        )),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    player_teams = player_teams_query.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not next(filter(lambda x: x.player_id == player.steam_id, player_teams)):
 | 
				
			||||||
 | 
					        abort(404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def map_role_to_schema(player_team_role: PlayerTeamRole):
 | 
				
			||||||
 | 
					        return ViewTeamMembersResponse.RoleSchema(
 | 
				
			||||||
 | 
					            role=str(player_team_role.role),
 | 
				
			||||||
 | 
					            is_main=player_team_role.is_main,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def map_to_response(player_team: PlayerTeam):
 | 
				
			||||||
 | 
					        roles = player_team.player_roles
 | 
				
			||||||
 | 
					        player = player_team.player
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        availability = 0
 | 
				
			||||||
 | 
					        if len(player_team.availability) > 0:
 | 
				
			||||||
 | 
					            print(player_team.availability)
 | 
				
			||||||
 | 
					            availability = player_team.availability[0].availability
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return ViewTeamMembersResponse(
 | 
				
			||||||
 | 
					            username=player.username,
 | 
				
			||||||
 | 
					            steam_id=str(player.steam_id),
 | 
				
			||||||
 | 
					            roles=list(map(map_role_to_schema, roles)),
 | 
				
			||||||
 | 
					            availability=availability,
 | 
				
			||||||
 | 
					            playtime=player_team.playtime.total_seconds() / 3600,
 | 
				
			||||||
 | 
					            created_at=player_team.created_at,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return list(map(map_to_response, player_teams))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue