Use surrogate keys for PlayerTeam models

master
John Montagu, the 4th Earl of Sandvich 2024-11-21 18:21:43 -08:00
parent 061499b822
commit ba2b568259
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
8 changed files with 253 additions and 47 deletions

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -4,7 +4,7 @@ from sqlalchemy.orm import mapped_column, relationship
from sqlalchemy.orm.attributes import Mapped from sqlalchemy.orm.attributes import Mapped
from sqlalchemy.orm.properties import ForeignKey from sqlalchemy.orm.properties import ForeignKey
from sqlalchemy.sql import func 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 app_db
import spec import spec
@ -16,8 +16,16 @@ class PlayerTeam(app_db.BaseModel):
Player = 0 Player = 0
CoachMentor = 1 CoachMentor = 1
player_id: Mapped[int] = mapped_column(ForeignKey("players.steam_id"), primary_key=True) # surrogate key
team_id: Mapped[int] = mapped_column(ForeignKey("teams.id"), primary_key=True) 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") player: Mapped["Player"] = relationship(back_populates="teams")
team: Mapped["Team"] = relationship(back_populates="players") team: Mapped["Team"] = relationship(back_populates="players")

View File

@ -1,8 +1,11 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sqlalchemy.orm.properties import ForeignKey
import spec import spec
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.schema import ForeignKeyConstraint from sqlalchemy.schema import ForeignKeyConstraint
from sqlalchemy.types import Integer from sqlalchemy.types import BigInteger, Integer
from sqlalchemy_utc import UtcDateTime from sqlalchemy_utc import UtcDateTime
import app_db import app_db
@ -10,25 +13,16 @@ import app_db
class PlayerTeamAvailability(app_db.BaseModel): class PlayerTeamAvailability(app_db.BaseModel):
__tablename__ = "players_teams_availability" __tablename__ = "players_teams_availability"
player_id: Mapped[int] = mapped_column(primary_key=True) player_team_id = mapped_column(ForeignKey("players_teams.id"), primary_key=True)
team_id: Mapped[int] = mapped_column(primary_key=True) start_time: Mapped[datetime] = mapped_column(UtcDateTime, primary_key=True, index=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, index=True)
from models.player_team import PlayerTeam
__table_args__ = (
ForeignKeyConstraint(
[player_id, team_id],
[PlayerTeam.player_id, PlayerTeam.team_id]
),
)
class AvailabilitySchema(spec.BaseModel): class AvailabilitySchema(spec.BaseModel):
steam_id: str steam_id: str
@ -48,7 +42,7 @@ class AvailabilitySchema(spec.BaseModel):
i = max(0, relative_start_hour) i = max(0, relative_start_hour)
while i < window_size_hours and i < relative_end_hour: while i < window_size_hours and i < relative_end_hour:
print(i, "=", region.availability) #print(i, "=", region.availability)
self.availability[i] = region.availability self.availability[i] = region.availability
i += 1 i += 1
@ -60,3 +54,6 @@ class PlayerTeamAvailabilityRoleSchema(spec.BaseModel):
playtime: int playtime: int
availability: int availability: int
roles: list[RoleSchema] roles: list[RoleSchema]
from models.player_team import PlayerTeam

View File

@ -1,9 +1,11 @@
import enum import enum
from sqlalchemy.orm.properties import ForeignKey
import spec import spec
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.schema import ForeignKeyConstraint from sqlalchemy.schema import ForeignKeyConstraint, UniqueConstraint
from sqlalchemy.types import Boolean, Enum from sqlalchemy.types import BigInteger, Boolean, Enum, Integer
import app_db import app_db
@ -29,25 +31,29 @@ class PlayerTeamRole(app_db.BaseModel):
Sniper = 12 Sniper = 12
Spy = 13 Spy = 13
player_id: Mapped[int] = mapped_column(primary_key=True) # surrogate key
team_id: Mapped[int] = mapped_column(primary_key=True) 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) is_main: Mapped[bool] = mapped_column(Boolean)
from models.player_team import PlayerTeam
__table_args__ = ( __table_args__ = (
ForeignKeyConstraint( UniqueConstraint("player_team_id", "role"),
[player_id, team_id],
[PlayerTeam.player_id, PlayerTeam.team_id]
),
) )
class RoleSchema(spec.BaseModel): class RoleSchema(spec.BaseModel):
role: str role: str
is_main: bool is_main: bool
@ -55,3 +61,6 @@ class RoleSchema(spec.BaseModel):
@classmethod @classmethod
def from_model(cls, role: PlayerTeamRole): def from_model(cls, role: PlayerTeamRole):
return cls(role=role.role.name, is_main=role.is_main) return cls(role=role.role.name, is_main=role.is_main)
from models.player_team import PlayerTeam

View File

@ -2,14 +2,14 @@ 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 spectree import Response from spectree import Response
from sqlalchemy.orm import joinedload from sqlalchemy.orm import contains_eager, joinedload
from sqlalchemy.sql import and_ from sqlalchemy.sql import and_, select
from app_db import db from app_db import db
from models.player import Player, PlayerSchema from models.player import Player, PlayerSchema
from models.player_team import PlayerTeam from models.player_team import PlayerTeam
from models.player_team_availability import AvailabilitySchema, PlayerTeamAvailability, PlayerTeamAvailabilityRoleSchema from models.player_team_availability import AvailabilitySchema, PlayerTeamAvailability, PlayerTeamAvailabilityRoleSchema
from models.player_team_role import PlayerTeamRole, RoleSchema 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 from spec import spec, BaseModel
@ -30,16 +30,15 @@ class ViewScheduleResponse(BaseModel):
) )
) )
@requires_authentication @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_start = query.window_start
window_end = window_start + datetime.timedelta(days=query.window_size_days) window_end = window_start + datetime.timedelta(days=query.window_size_days)
availability_regions = db.session.query( availability_regions = db.session.query(
PlayerTeamAvailability PlayerTeamAvailability
).where( ).where(
PlayerTeamAvailability.player_id == player.steam_id PlayerTeamAvailability.player_team_id == player_team.id
).where(
PlayerTeamAvailability.team_id == query.team_id
).where( ).where(
PlayerTeamAvailability.start_time.between(window_start, window_end) | PlayerTeamAvailability.start_time.between(window_start, window_end) |
PlayerTeamAvailability.end_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("/") @api_schedule.put("/")
@spec.validate() @spec.validate()
@requires_authentication @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_start = json.window_start
window_end = window_start + datetime.timedelta(days=json.window_size_days) 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( cur_availability = db.session.query(
PlayerTeamAvailability PlayerTeamAvailability
).where( ).where(
PlayerTeamAvailability.player_id == player.steam_id PlayerTeamAvailability.player_team == player_team
).where(
PlayerTeamAvailability.team_id == json.team_id
).where( ).where(
PlayerTeamAvailability.start_time.between(window_start, window_end) | PlayerTeamAvailability.start_time.between(window_start, window_end) |
PlayerTeamAvailability.end_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.availability = availability_value
new_availability.start_time = abs_start new_availability.start_time = abs_start
new_availability.end_time = abs_end new_availability.end_time = abs_end
new_availability.player_id = player.steam_id new_availability.player_team = player_team
new_availability.team_id = json.team_id
availability_blocks.append(new_availability) availability_blocks.append(new_availability)
@ -206,6 +203,7 @@ def get_team_availability(query: ViewScheduleForm, player: Player, **kwargs):
).outerjoin( ).outerjoin(
PlayerTeamAvailability, PlayerTeamAvailability,
and_( and_(
PlayerTeamAvailability.player_team_id == PlayerTeam.id,
PlayerTeamAvailability.start_time.between(window_start, window_end) | PlayerTeamAvailability.start_time.between(window_start, window_end) |
PlayerTeamAvailability.end_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 Player
).where( ).where(
PlayerTeam.team_id == query.team_id 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() ).all()
ret: dict[str, AvailabilitySchema] = { } ret: dict[str, AvailabilitySchema] = { }
@ -264,7 +268,7 @@ def view_available_at_time(query: ViewAvailablePlayersQuery, player: Player, **k
).join( ).join(
PlayerTeamRole PlayerTeamRole
).where( ).where(
PlayerTeamAvailability.team_id == query.team_id PlayerTeam.team_id == query.team_id
).where( ).where(
(PlayerTeamAvailability.start_time <= start_time) & (PlayerTeamAvailability.start_time <= start_time) &
(PlayerTeamAvailability.end_time > start_time) (PlayerTeamAvailability.end_time > start_time)