Files
team-tryouts/app/timeslots.py
T
cedrick2711 bfb25f4d16
CI - Security, Lint & Tests / validate (push) Failing after 1m3s
ajout d'un toggle pour permission d'upload des documents dans admin pannel
ajout de compteur d'heure disponible
ajout du calendrier de dispo dans les profils publics
2026-08-26 22:33:24 -04:00

97 lines
3.7 KiB
Python

"""The half-hour slot, in one place (MNT-12).
Availability, coach bookings and matches are all built from the same rule:
*a slot given a start and no end lasts thirty minutes*. It was written five
times, in four modules, and the copies had drifted in the way that matters —
not in the arithmetic, but in what each did when the input was wrong:
matches.py flashed an error and redirected
matches.py, later set start_time to None and reported success
team_matches.py `except ValueError: pass`, silently
availability.py `continue`, dropping the slot without a word
Waves E and G closed the match side by putting a marshmallow schema at the
form boundary. This module is the other half: the constant and the two
functions the remaining callers need, so that the availability routes can
use the same schema treatment without each one re-deciding what a slot is.
`DEFAULT_SLOT_MINUTES` is deliberately not configurable. It is a product
decision written into the UI — the availability grid draws half-hour cells —
and a setting would let the two disagree.
"""
from datetime import date as date_cls
from datetime import datetime, timedelta
#: How long a slot lasts when only its start is given.
DEFAULT_SLOT_MINUTES = 30
#: Monday-first, matching `datetime.weekday()` and the availability grid.
DAY_NAMES = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday')
def slot_end(start_time, minutes=DEFAULT_SLOT_MINUTES, on=None):
"""The end of a slot starting at `start_time`.
Args:
start_time: A `datetime.time`.
minutes: Slot length; defaults to DEFAULT_SLOT_MINUTES.
on: The date the slot falls on. Only matters for a slot that would
cross midnight, where the arbitrary date the old helpers used
(`datetime.today()`) made the result depend on when the code ran.
Returns:
datetime.time: The end of the slot, wrapping past midnight like the
previous implementations did.
"""
anchor = on or date_cls(2000, 1, 1)
return (datetime.combine(anchor, start_time) + timedelta(minutes=minutes)).time()
def day_name(day_of_week):
"""Name of a weekday index, or a readable fallback.
Every caller indexed a module-level list directly, so an out-of-range
day — which nothing prevented before the schemas — was an IndexError
inside a JSON route, i.e. a 500 with an HTML body.
"""
if 0 <= day_of_week < len(DAY_NAMES):
return DAY_NAMES[day_of_week]
return f'Day {day_of_week}'
def slot_duration_minutes(start_time, end_time):
"""Minutes a slot spans, wrapping past midnight like slot_end does.
Works on SQLite where ``Time`` columns are stored as text — the
arithmetic stays in pure Python so it is portable across databases.
Args:
start_time: A ``datetime.time``.
end_time: A ``datetime.time`` (may be earlier if the slot wraps
past midnight).
Returns:
int: Duration in whole minutes.
"""
anchor = date_cls(2000, 1, 1)
start_dt = datetime.combine(anchor, start_time)
end_dt = datetime.combine(anchor, end_time)
if end_dt <= start_dt:
end_dt += timedelta(days=1)
return int((end_dt - start_dt).total_seconds() // 60)
def total_hours(slots):
"""Aggregate slot durations into a single float, rounded to one decimal.
Args:
slots: Iterable of objects with ``start_time`` and ``end_time``
attributes (``PlayerDisponibility`` or ``CoachAvailability``).
Returns:
float: Total hours across all slots, e.g. ``8.5``.
"""
total_minutes = sum(slot_duration_minutes(s.start_time, s.end_time) for s in slots)
return round(total_minutes / 60, 1)