fix(data): une saisie d heure refusee n efface plus la disponibilite

MNT-12. Le meme bloc de parsing date/heure vivait dans quatre modules avec
trois reponses differentes a la meme saisie invalide : signaler et rediriger,
mettre la valeur a None et annoncer la reussite, ou passer au suivant en
silence. Les vagues E et G ont ferme le cote matchs avec des schemas ; il
restait la disponibilite, les creneaux de coach et les demandes individuelles.

Deux choses trouvees en appliquant, aucune dans le constat.

OneOnOneRequestSchema et DisponibilityAddSchema etaient definis dans
validators.py et appeles NULLE PART : aucun import, aucun test. C'est le
motif SEC-AUTHZ-001 -- une politique de validation ecrite et non appliquee --
qui survivait dans un coin que personne n'avait rouvert. Les deux passaient
d'ailleurs par des fields.String + Regexp, qui verifient la forme et laissent
l'appelant convertir ; fields.Date et fields.Time font les deux.

Et manage_coach_availability supprimait tous les creneaux existants avant de
reajouter ceux qu'il savait lire, ignorant les autres en silence et repondant
{'success': true}. Un envoi malforme effacait donc les heures reservables
d'un coach en annoncant la reussite -- et les demandes individuelles sont
refusees contre exactement cette table, donc le coach devenait injoignable
sans que rien ne le dise. Une operation de remplacement doit tout valider
avant de rien supprimer : le lot est refuse en entier.

Trouve aussi : le controle de disponibilite comparait les chaines du
formulaire aux chaines serialisees, ce qui ne marchait que parce que les deux
cotes etaient en HH:MM a zero non significatif. La comparaison porte
desormais sur des objets time.

Et le lint a rattrape une regression que la relecture avait manquee --
sixieme fois : datetime retire de one_on_one.py alors que deux fonctions non
touchees l'utilisaient encore.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
GGThed
2026-08-11 20:20:25 -04:00
co-authored by Claude Opus 5
parent 9cedf4a038
commit 9166d8abeb
5 changed files with 539 additions and 111 deletions
+60
View File
@@ -0,0 +1,60 @@
"""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}'