292 lines
10 KiB
Python
292 lines
10 KiB
Python
import datetime
|
|
from typing import cast
|
|
from flask import Blueprint, abort, jsonify, make_response, request
|
|
from spectree import Response
|
|
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, requires_team_membership
|
|
from spec import spec, BaseModel
|
|
|
|
|
|
api_schedule = Blueprint("schedule", __name__, url_prefix="/schedule")
|
|
|
|
class ViewScheduleForm(BaseModel):
|
|
window_start: datetime.datetime
|
|
team_id: int
|
|
window_size_days: int = 7
|
|
|
|
class ViewScheduleResponse(BaseModel):
|
|
availability: list[int]
|
|
|
|
@api_schedule.get("/")
|
|
@spec.validate(
|
|
resp=Response(
|
|
HTTP_200=ViewScheduleResponse
|
|
)
|
|
)
|
|
@requires_authentication
|
|
@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_team_id == player_team.id
|
|
).where(
|
|
PlayerTeamAvailability.start_time.between(window_start, window_end) |
|
|
PlayerTeamAvailability.end_time.between(window_start, window_end) |
|
|
|
|
# handle edge case where someone for some reason might list their
|
|
# availability spanning more than a week total
|
|
((PlayerTeamAvailability.start_time < window_start) &
|
|
(PlayerTeamAvailability.end_time > window_end))
|
|
).all()
|
|
|
|
window_size_hours = 24 * query.window_size_days
|
|
availability = [0] * window_size_hours
|
|
for region in availability_regions:
|
|
region: PlayerTeamAvailability
|
|
|
|
# this is the start time relative to the window (as timedelta)
|
|
#relative_start_time = (region.start_time.replace(tzinfo=utc.utc) - window_start)
|
|
#relative_start_hour = int(relative_start_time.total_seconds() // 3600)
|
|
#relative_end_time = (region.end_time.replace(tzinfo=utc.utc) - window_start)
|
|
#relative_end_hour = int(relative_end_time.total_seconds() // 3600)
|
|
|
|
relative_start_time = region.start_time - window_start
|
|
relative_start_hour = int(relative_start_time.total_seconds() // 3600)
|
|
relative_end_time = region.end_time - window_start
|
|
relative_end_hour = int(relative_end_time.total_seconds() // 3600)
|
|
|
|
i = max(0, relative_start_hour)
|
|
while i < window_size_hours and i < relative_end_hour:
|
|
print(i, "=", region.availability)
|
|
availability[i] = region.availability
|
|
i += 1
|
|
|
|
return {
|
|
"availability": availability
|
|
}
|
|
|
|
class PutScheduleForm(ViewScheduleForm):
|
|
availability: list[int]
|
|
|
|
def find_consecutive_blocks(arr: list[int]) -> list[tuple[int, int, int]]:
|
|
blocks: list[tuple[int, int, int]] = []
|
|
current_block_value = 0
|
|
current_block_start = 0
|
|
|
|
for i in range(len(arr)):
|
|
if arr[i] != current_block_value:
|
|
# we find a different value
|
|
if current_block_value > 0:
|
|
blocks.append((current_block_value, current_block_start, i))
|
|
# begin a new block
|
|
current_block_start = i
|
|
current_block_value = arr[i]
|
|
|
|
if current_block_value > 0:
|
|
blocks.append((current_block_value, current_block_start, len(arr)))
|
|
|
|
return blocks
|
|
|
|
@api_schedule.put("/")
|
|
@spec.validate()
|
|
@requires_authentication
|
|
@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)
|
|
|
|
# TODO: add error message
|
|
if len(json.availability) != 168:
|
|
abort(400, {
|
|
"error": "Availability must be length " + str(168)
|
|
})
|
|
|
|
cur_availability = db.session.query(
|
|
PlayerTeamAvailability
|
|
).where(
|
|
PlayerTeamAvailability.player_team == player_team
|
|
).where(
|
|
PlayerTeamAvailability.start_time.between(window_start, window_end) |
|
|
PlayerTeamAvailability.end_time.between(window_start, window_end)
|
|
).order_by(
|
|
PlayerTeamAvailability.start_time
|
|
).all()
|
|
|
|
# cut the availability times so that they do not intersect our window
|
|
if len(cur_availability) > 0:
|
|
if cur_availability[0].start_time < window_start:
|
|
if cur_availability[0].end_time > window_end:
|
|
# if the availability overlaps the entire window, duplicate it
|
|
# this way, we can trim the start_time of the duplicate
|
|
cur_availability.append(cur_availability[0])
|
|
cur_availability[0].end_time = window_start
|
|
if cur_availability[-1].end_time > window_end:
|
|
cur_availability[-1].start_time = window_end
|
|
|
|
# remove all availability regions strictly inside window
|
|
i = 0
|
|
for region in cur_availability[:]:
|
|
if region.start_time >= window_start and region.end_time <= window_end:
|
|
print("Deleting", region)
|
|
db.session.delete(region)
|
|
cur_availability.pop(i)
|
|
else:
|
|
i += 1
|
|
|
|
if len(cur_availability) > 2:
|
|
# this is not supposed to happen
|
|
db.session.rollback()
|
|
raise ValueError()
|
|
|
|
# create time regions inside our window based on the availability array
|
|
availability_blocks = []
|
|
|
|
for block in find_consecutive_blocks(json.availability):
|
|
availability_value = block[0]
|
|
hour_start = block[1]
|
|
hour_end = block[2]
|
|
|
|
abs_start = window_start + datetime.timedelta(hours=hour_start)
|
|
abs_end = window_start + datetime.timedelta(hours=hour_end)
|
|
|
|
print("Create availability from", abs_start, "to", abs_end)
|
|
|
|
new_availability = PlayerTeamAvailability()
|
|
new_availability.availability = availability_value
|
|
new_availability.start_time = abs_start
|
|
new_availability.end_time = abs_end
|
|
new_availability.player_team = player_team
|
|
|
|
availability_blocks.append(new_availability)
|
|
|
|
# merge availability blocks if needed
|
|
if len(cur_availability) > 0 and len(availability_blocks) > 0:
|
|
if availability_blocks[0].start_time == cur_availability[0].end_time:
|
|
cur_availability[0].end_time = availability_blocks[0].end_time
|
|
availability_blocks.pop(0)
|
|
|
|
if len(cur_availability) > 0 and len(availability_blocks) > 0:
|
|
if availability_blocks[-1].end_time == cur_availability[-1].start_time:
|
|
cur_availability[-1].start_time = availability_blocks[-1].start_time
|
|
availability_blocks.pop(-1)
|
|
|
|
db.session.add_all(availability_blocks)
|
|
db.session.commit()
|
|
return make_response({ }, 200)
|
|
|
|
class ViewTeamScheduleResponse(BaseModel):
|
|
player_availability: dict[str, AvailabilitySchema]
|
|
|
|
@api_schedule.get("/team")
|
|
@spec.validate(
|
|
resp=Response(
|
|
HTTP_200=ViewTeamScheduleResponse
|
|
)
|
|
)
|
|
@requires_authentication
|
|
def get_team_availability(query: ViewScheduleForm, player: Player, **kwargs):
|
|
window_start = query.window_start
|
|
window_end = window_start + datetime.timedelta(days=query.window_size_days)
|
|
|
|
players_teams = db.session.query(
|
|
PlayerTeam
|
|
).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) |
|
|
|
|
# handle edge case where someone for some reason might list their
|
|
# availability spanning more than a week total
|
|
((PlayerTeamAvailability.start_time < window_start) &
|
|
(PlayerTeamAvailability.end_time > window_end))
|
|
)
|
|
).join(
|
|
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] = { }
|
|
|
|
for player_team in players_teams:
|
|
player_id = str(player_team.player_id)
|
|
|
|
ret[player_id] = AvailabilitySchema(
|
|
steam_id=player_id,
|
|
username=player_team.player.username,
|
|
)
|
|
|
|
for region in player_team.availability:
|
|
ret[player_id].add_availability_region(region, window_start)
|
|
|
|
return ViewTeamScheduleResponse(
|
|
player_availability=ret,
|
|
).dict(by_alias=True)
|
|
|
|
class ViewAvailablePlayersQuery(BaseModel):
|
|
start_time: datetime.datetime
|
|
team_id: int
|
|
|
|
class ViewAvailablePlayersResponse(BaseModel):
|
|
players: list[PlayerTeamAvailabilityRoleSchema]
|
|
|
|
@api_schedule.get("/view-available")
|
|
@spec.validate(
|
|
resp=Response(
|
|
HTTP_200=ViewAvailablePlayersResponse,
|
|
),
|
|
operation_id="view_available_at_time"
|
|
)
|
|
@requires_authentication
|
|
def view_available_at_time(query: ViewAvailablePlayersQuery, player: Player, **kwargs):
|
|
start_time = query.start_time
|
|
|
|
records = db.session.query(
|
|
PlayerTeamAvailability
|
|
).join(
|
|
PlayerTeam
|
|
).join(
|
|
Player
|
|
).join(
|
|
PlayerTeamRole
|
|
).where(
|
|
PlayerTeam.team_id == query.team_id
|
|
).where(
|
|
(PlayerTeamAvailability.start_time <= start_time) &
|
|
(PlayerTeamAvailability.end_time > start_time)
|
|
).all()
|
|
|
|
def map_to_response(player_avail: PlayerTeamAvailability):
|
|
player_team = player_avail.player_team
|
|
player = player_team.player
|
|
player_roles = player_team.player_roles
|
|
|
|
return PlayerTeamAvailabilityRoleSchema(
|
|
player=PlayerSchema.from_model(player),
|
|
playtime=int(player_team.playtime.total_seconds()),
|
|
availability=player_avail.availability,
|
|
roles=list(map(RoleSchema.from_model, player_roles)),
|
|
)
|
|
|
|
return ViewAvailablePlayersResponse(
|
|
players=list(map(map_to_response, records))
|
|
).dict(by_alias=True), 200
|