availabili.tf/backend-flask/schedule.py

227 lines
8.2 KiB
Python

import datetime
from typing import cast
from flask import Blueprint, abort, jsonify, make_response, request
from flask_pydantic import validate
from spectree import Response
from models import Player, PlayerTeam, PlayerTeamAvailability, PlayerTeamRole, db
from middleware import requires_authentication
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
def get(query: ViewScheduleForm, player: Player, **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
).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(BaseModel):
window_start: datetime.datetime
window_size_days: int = 7
team_id: int
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
def put(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_id == player.steam_id
).where(
PlayerTeamAvailability.team_id == json.team_id
).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_id = player.steam_id
new_availability.team_id = json.team_id
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 ViewAvailablePlayersForm(BaseModel):
start_time: datetime.datetime
team_id: int
@api_schedule.get("/view-available")
@spec.validate()
@requires_authentication
def view_available(query: ViewAvailablePlayersForm, player: Player, **kwargs):
start_time = query.start_time
availability = db.session.query(
PlayerTeamAvailability
).where(
PlayerTeamAvailability.player_id == player.steam_id
).where(
PlayerTeamAvailability.team_id == query.team_id
).where(
(PlayerTeamAvailability.start_time <= start_time) &
(PlayerTeamAvailability.end_time > start_time)
).all()
def map_roles_to_json(roles: list[PlayerTeamRole],
player_team: PlayerTeam,
entry: PlayerTeamAvailability):
for role in roles:
yield {
"steamId": entry.player_id,
"username": entry.player_team.player.username,
"role": role.role.name,
"isMain": role.is_main,
"availability": entry.availability,
"playtime": int(player_team.playtime.total_seconds()),
}
def map_availability_to_json(entry: PlayerTeamAvailability):
player_team = entry.player_team
player_roles = player_team.player_roles
return list(map_roles_to_json(player_roles, player_team, entry))
return jsonify(list(map(map_availability_to_json, availability)))