diff --git a/backend-flask/migrations/versions/47f0722b02b0_add_availability_index.py b/backend-flask/migrations/versions/47f0722b02b0_add_availability_index.py new file mode 100644 index 0000000..7db076f --- /dev/null +++ b/backend-flask/migrations/versions/47f0722b02b0_add_availability_index.py @@ -0,0 +1,34 @@ +"""Add availability index + +Revision ID: 47f0722b02b0 +Revises: 7361c978e53d +Create Date: 2024-11-21 13:10:45.098947 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '47f0722b02b0' +down_revision = '7361c978e53d' +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.create_index(batch_op.f('ix_players_teams_availability_end_time'), ['end_time'], unique=False) + batch_op.create_index(batch_op.f('ix_players_teams_availability_start_time'), ['start_time'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('players_teams_availability', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_players_teams_availability_start_time')) + batch_op.drop_index(batch_op.f('ix_players_teams_availability_end_time')) + + # ### end Alembic commands ### diff --git a/backend-flask/migrations/versions/6296c347731b_add_surrogate_key_to_player_team_and_.py b/backend-flask/migrations/versions/6296c347731b_add_surrogate_key_to_player_team_and_.py new file mode 100644 index 0000000..a7108a4 --- /dev/null +++ b/backend-flask/migrations/versions/6296c347731b_add_surrogate_key_to_player_team_and_.py @@ -0,0 +1,64 @@ +"""Add surrogate key to player_team and others + +Revision ID: 6296c347731b +Revises: 5debac4cdf37 +Create Date: 2024-11-21 10:30:09.333087 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6296c347731b' +down_revision = '5debac4cdf37' +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('player_team_id', sa.Integer(), autoincrement=True, nullable=False)) + + with op.batch_alter_table('players_teams_availability', schema=None) as batch_op: + batch_op.add_column(sa.Column('player_team_id', sa.Integer(), nullable=False)) + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key(None, 'players_teams', ['player_team_id'], ['player_team_id']) + batch_op.drop_column('team_id') + batch_op.drop_column('player_id') + + with op.batch_alter_table('players_teams_roles', schema=None) as batch_op: + batch_op.add_column(sa.Column('player_team_role_id', sa.Integer(), autoincrement=True, nullable=False)) + batch_op.add_column(sa.Column('player_team_id', sa.Integer(), nullable=False)) + batch_op.create_unique_constraint(None, ['player_team_id', 'role']) + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key(None, 'players_teams', ['player_team_id'], ['player_team_id']) + batch_op.drop_column('team_id') + batch_op.drop_column('player_id') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('players_teams_roles', schema=None) as batch_op: + batch_op.add_column(sa.Column('player_id', sa.INTEGER(), nullable=False)) + batch_op.add_column(sa.Column('team_id', sa.INTEGER(), nullable=False)) + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key(None, 'players_teams', ['player_id', 'team_id'], ['player_id', 'team_id']) + batch_op.drop_constraint(None, type_='unique') + batch_op.drop_column('player_team_id') + batch_op.drop_column('player_team_role_id') + + with op.batch_alter_table('players_teams_availability', schema=None) as batch_op: + batch_op.add_column(sa.Column('player_id', sa.INTEGER(), nullable=False)) + batch_op.add_column(sa.Column('team_id', sa.INTEGER(), nullable=False)) + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key(None, 'players_teams', ['player_id', 'team_id'], ['player_id', 'team_id']) + batch_op.drop_column('player_team_id') + + with op.batch_alter_table('players_teams', schema=None) as batch_op: + batch_op.drop_column('player_team_id') + + # ### end Alembic commands ### diff --git a/backend-flask/migrations/versions/6e9d70f835d7_change_player_team_surrogate_key_name.py b/backend-flask/migrations/versions/6e9d70f835d7_change_player_team_surrogate_key_name.py new file mode 100644 index 0000000..da98c1b --- /dev/null +++ b/backend-flask/migrations/versions/6e9d70f835d7_change_player_team_surrogate_key_name.py @@ -0,0 +1,50 @@ +"""Change player_team surrogate key name + +Revision ID: 6e9d70f835d7 +Revises: 6296c347731b +Create Date: 2024-11-21 12:13:44.989797 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6e9d70f835d7' +down_revision = '6296c347731b' +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('id', sa.Integer(), autoincrement=True, nullable=False)) + batch_op.drop_column('player_team_id') + + with op.batch_alter_table('players_teams_availability', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key(None, 'players_teams', ['player_team_id'], ['id']) + + with op.batch_alter_table('players_teams_roles', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key(None, 'players_teams', ['player_team_id'], ['id']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('players_teams_roles', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key(None, 'players_teams', ['player_team_id'], ['player_team_id']) + + with op.batch_alter_table('players_teams_availability', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key(None, 'players_teams', ['player_team_id'], ['player_team_id']) + + with op.batch_alter_table('players_teams', schema=None) as batch_op: + batch_op.add_column(sa.Column('player_team_id', sa.INTEGER(), nullable=False)) + batch_op.drop_column('id') + + # ### end Alembic commands ### diff --git a/backend-flask/migrations/versions/7361c978e53d_fix_integrity.py b/backend-flask/migrations/versions/7361c978e53d_fix_integrity.py new file mode 100644 index 0000000..7fe8a65 --- /dev/null +++ b/backend-flask/migrations/versions/7361c978e53d_fix_integrity.py @@ -0,0 +1,40 @@ +"""Fix integrity + +Revision ID: 7361c978e53d +Revises: 6e9d70f835d7 +Create Date: 2024-11-21 12:43:01.786598 + +""" +from alembic import op +import sqlalchemy as sa + +import app_db + + +# revision identifiers, used by Alembic. +revision = '7361c978e53d' +down_revision = '6e9d70f835d7' +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, naming_convention=app_db.convention) as batch_op: + batch_op.create_foreign_key(batch_op.f("fk_players_teams_availability_player_team_id_players_teams"), 'players_teams', ['player_team_id'], ['id']) + + with op.batch_alter_table('players_teams_roles', schema=None) as batch_op: + batch_op.create_foreign_key(batch_op.f("fk_players_teams_roles_player_team_id_players_teams"), 'players_teams', ['player_team_id'], ['id']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('players_teams_roles', schema=None, naming_convention=app_db.convention) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + + with op.batch_alter_table('players_teams_availability', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + + # ### end Alembic commands ### diff --git a/backend-flask/models/player_team.py b/backend-flask/models/player_team.py index f00bffc..c061598 100644 --- a/backend-flask/models/player_team.py +++ b/backend-flask/models/player_team.py @@ -4,7 +4,7 @@ from sqlalchemy.orm import mapped_column, relationship from sqlalchemy.orm.attributes import Mapped from sqlalchemy.orm.properties import ForeignKey from sqlalchemy.sql import func -from sqlalchemy.types import TIMESTAMP, Boolean, Enum, Interval +from sqlalchemy.types import TIMESTAMP, Boolean, Enum, Integer, Interval import app_db import spec @@ -16,8 +16,16 @@ class PlayerTeam(app_db.BaseModel): Player = 0 CoachMentor = 1 - player_id: Mapped[int] = mapped_column(ForeignKey("players.steam_id"), primary_key=True) - team_id: Mapped[int] = mapped_column(ForeignKey("teams.id"), primary_key=True) + # surrogate key + id: Mapped[int] = mapped_column( + Integer, + autoincrement=True, + primary_key=True, + ) + + # primary key + player_id: Mapped[int] = mapped_column(ForeignKey("players.steam_id")) + team_id: Mapped[int] = mapped_column(ForeignKey("teams.id")) player: Mapped["Player"] = relationship(back_populates="teams") team: Mapped["Team"] = relationship(back_populates="players") diff --git a/backend-flask/models/player_team_availability.py b/backend-flask/models/player_team_availability.py index 270152d..9790190 100644 --- a/backend-flask/models/player_team_availability.py +++ b/backend-flask/models/player_team_availability.py @@ -1,8 +1,11 @@ from datetime import datetime, timedelta + +from sqlalchemy.orm.properties import ForeignKey + import spec from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.schema import ForeignKeyConstraint -from sqlalchemy.types import Integer +from sqlalchemy.types import BigInteger, Integer from sqlalchemy_utc import UtcDateTime import app_db @@ -10,25 +13,16 @@ import app_db class PlayerTeamAvailability(app_db.BaseModel): __tablename__ = "players_teams_availability" - player_id: Mapped[int] = mapped_column(primary_key=True) - team_id: Mapped[int] = mapped_column(primary_key=True) - start_time: Mapped[datetime] = mapped_column(UtcDateTime, primary_key=True) + player_team_id = mapped_column(ForeignKey("players_teams.id"), primary_key=True) + start_time: Mapped[datetime] = mapped_column(UtcDateTime, primary_key=True, index=True) player_team: Mapped["PlayerTeam"] = relationship( - "PlayerTeam", back_populates="availability") + "PlayerTeam", + back_populates="availability", + ) availability: Mapped[int] = mapped_column(Integer, default=2) - end_time: Mapped[datetime] = mapped_column(UtcDateTime) - - - from models.player_team import PlayerTeam - - __table_args__ = ( - ForeignKeyConstraint( - [player_id, team_id], - [PlayerTeam.player_id, PlayerTeam.team_id] - ), - ) + end_time: Mapped[datetime] = mapped_column(UtcDateTime, index=True) class AvailabilitySchema(spec.BaseModel): steam_id: str @@ -48,7 +42,7 @@ class AvailabilitySchema(spec.BaseModel): i = max(0, relative_start_hour) while i < window_size_hours and i < relative_end_hour: - print(i, "=", region.availability) + #print(i, "=", region.availability) self.availability[i] = region.availability i += 1 @@ -60,3 +54,6 @@ class PlayerTeamAvailabilityRoleSchema(spec.BaseModel): playtime: int availability: int roles: list[RoleSchema] + + +from models.player_team import PlayerTeam diff --git a/backend-flask/models/player_team_role.py b/backend-flask/models/player_team_role.py index 15784f6..3a095a2 100644 --- a/backend-flask/models/player_team_role.py +++ b/backend-flask/models/player_team_role.py @@ -1,9 +1,11 @@ import enum +from sqlalchemy.orm.properties import ForeignKey + import spec from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.schema import ForeignKeyConstraint -from sqlalchemy.types import Boolean, Enum +from sqlalchemy.schema import ForeignKeyConstraint, UniqueConstraint +from sqlalchemy.types import BigInteger, Boolean, Enum, Integer import app_db @@ -29,25 +31,29 @@ class PlayerTeamRole(app_db.BaseModel): Sniper = 12 Spy = 13 - player_id: Mapped[int] = mapped_column(primary_key=True) - team_id: Mapped[int] = mapped_column(primary_key=True) + # surrogate key + player_team_role_id: Mapped[int] = mapped_column( + Integer, + autoincrement=True, + primary_key=True, + ) - player_team: Mapped["PlayerTeam"] = relationship("PlayerTeam", back_populates="player_roles") + # primary key + player_team_id = mapped_column(ForeignKey("players_teams.id"), nullable=False) + role: Mapped[Role] = mapped_column(Enum(Role), nullable=False) - #player: Mapped["Player"] = relationship(back_populates="teams") + player_team: Mapped["PlayerTeam"] = relationship( + "PlayerTeam", + back_populates="player_roles" + ) - role: Mapped[Role] = mapped_column(Enum(Role), primary_key=True) is_main: Mapped[bool] = mapped_column(Boolean) - from models.player_team import PlayerTeam - __table_args__ = ( - ForeignKeyConstraint( - [player_id, team_id], - [PlayerTeam.player_id, PlayerTeam.team_id] - ), + UniqueConstraint("player_team_id", "role"), ) + class RoleSchema(spec.BaseModel): role: str is_main: bool @@ -55,3 +61,6 @@ class RoleSchema(spec.BaseModel): @classmethod def from_model(cls, role: PlayerTeamRole): return cls(role=role.role.name, is_main=role.is_main) + + +from models.player_team import PlayerTeam diff --git a/backend-flask/schedule.py b/backend-flask/schedule.py index 5de99a5..06b611a 100644 --- a/backend-flask/schedule.py +++ b/backend-flask/schedule.py @@ -2,14 +2,14 @@ import datetime from typing import cast from flask import Blueprint, abort, jsonify, make_response, request from spectree import Response -from sqlalchemy.orm import joinedload -from sqlalchemy.sql import and_ +from sqlalchemy.orm import contains_eager, joinedload +from sqlalchemy.sql import and_, select from app_db import db from models.player import Player, PlayerSchema from models.player_team import PlayerTeam from models.player_team_availability import AvailabilitySchema, PlayerTeamAvailability, PlayerTeamAvailabilityRoleSchema from models.player_team_role import PlayerTeamRole, RoleSchema -from middleware import requires_authentication +from middleware import requires_authentication, requires_team_membership from spec import spec, BaseModel @@ -30,16 +30,15 @@ class ViewScheduleResponse(BaseModel): ) ) @requires_authentication -def get(query: ViewScheduleForm, player: Player, **kwargs): +@requires_team_membership(query_param="team_id") +def get(query: ViewScheduleForm, player_team: PlayerTeam, **kwargs): window_start = query.window_start window_end = window_start + datetime.timedelta(days=query.window_size_days) availability_regions = db.session.query( PlayerTeamAvailability ).where( - PlayerTeamAvailability.player_id == player.steam_id - ).where( - PlayerTeamAvailability.team_id == query.team_id + PlayerTeamAvailability.player_team_id == player_team.id ).where( PlayerTeamAvailability.start_time.between(window_start, window_end) | PlayerTeamAvailability.end_time.between(window_start, window_end) | @@ -101,7 +100,8 @@ def find_consecutive_blocks(arr: list[int]) -> list[tuple[int, int, int]]: @api_schedule.put("/") @spec.validate() @requires_authentication -def put(json: PutScheduleForm, player: Player, **kwargs): +@requires_team_membership(json_param="team_id") +def put(player_team: PlayerTeam, json: PutScheduleForm, player: Player, **kwargs): window_start = json.window_start window_end = window_start + datetime.timedelta(days=json.window_size_days) @@ -114,9 +114,7 @@ def put(json: PutScheduleForm, player: Player, **kwargs): cur_availability = db.session.query( PlayerTeamAvailability ).where( - PlayerTeamAvailability.player_id == player.steam_id - ).where( - PlayerTeamAvailability.team_id == json.team_id + PlayerTeamAvailability.player_team == player_team ).where( PlayerTeamAvailability.start_time.between(window_start, window_end) | PlayerTeamAvailability.end_time.between(window_start, window_end) @@ -167,8 +165,7 @@ def put(json: PutScheduleForm, player: Player, **kwargs): new_availability.availability = availability_value new_availability.start_time = abs_start new_availability.end_time = abs_end - new_availability.player_id = player.steam_id - new_availability.team_id = json.team_id + new_availability.player_team = player_team availability_blocks.append(new_availability) @@ -206,6 +203,7 @@ def get_team_availability(query: ViewScheduleForm, player: Player, **kwargs): ).outerjoin( PlayerTeamAvailability, and_( + PlayerTeamAvailability.player_team_id == PlayerTeam.id, PlayerTeamAvailability.start_time.between(window_start, window_end) | PlayerTeamAvailability.end_time.between(window_start, window_end) | @@ -218,6 +216,12 @@ def get_team_availability(query: ViewScheduleForm, player: Player, **kwargs): Player ).where( PlayerTeam.team_id == query.team_id + ).options( + # only populate PlayerTeam.availability with the availability regions + # that are within the window + contains_eager(PlayerTeam.availability), + joinedload(PlayerTeam.player), + ).populate_existing( ).all() ret: dict[str, AvailabilitySchema] = { } @@ -264,7 +268,7 @@ def view_available_at_time(query: ViewAvailablePlayersQuery, player: Player, **k ).join( PlayerTeamRole ).where( - PlayerTeamAvailability.team_id == query.team_id + PlayerTeam.team_id == query.team_id ).where( (PlayerTeamAvailability.start_time <= start_time) & (PlayerTeamAvailability.end_time > start_time)