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:
+69
-21
@@ -461,28 +461,41 @@ class UploadContractSchema(StripMixin):
|
||||
|
||||
|
||||
class OneOnOneRequestSchema(StripMixin):
|
||||
"""Validate One on One session request form input.
|
||||
"""A player asking their coach for a session (MNT-12).
|
||||
|
||||
Fields:
|
||||
date: Date string (YYYY-MM-DD), required.
|
||||
start_time: Time string (HH:MM), required.
|
||||
end_time: Time string (HH:MM), required.
|
||||
points: Optional text.
|
||||
This schema existed before and **was never called**: not imported by any
|
||||
route, not exercised by any test. `one_on_one.py` parsed the same three
|
||||
fields by hand, three lines apart, and parsed the date a second time to
|
||||
get its weekday. That is the pattern the audit named as SEC-AUTHZ-001 —
|
||||
a validation policy that is written down and not applied — surviving in
|
||||
a corner nobody had looked at since.
|
||||
|
||||
The fields were `String` + `Regexp`, which checked the shape and left the
|
||||
caller to convert. `Date` and `Time` do both, so the route receives the
|
||||
objects it is going to store and the "is this a date" question is asked
|
||||
once, in one place.
|
||||
"""
|
||||
|
||||
date = fields.String(
|
||||
date = fields.Date(
|
||||
required=True,
|
||||
validate=validate.Regexp(
|
||||
r'^\d{4}-\d{2}-\d{2}$', error=_l('Date must be in YYYY-MM-DD format.')
|
||||
),
|
||||
error_messages={
|
||||
'invalid': _l('Date must be in YYYY-MM-DD format.'),
|
||||
'required': _l('A date is required.'),
|
||||
},
|
||||
)
|
||||
start_time = fields.String(
|
||||
start_time = fields.Time(
|
||||
required=True,
|
||||
validate=validate.Regexp(r'^\d{2}:\d{2}$', error=_l('Start time must be in HH:MM format.')),
|
||||
error_messages={
|
||||
'invalid': _l('Start time must be in HH:MM format.'),
|
||||
'required': _l('A start time is required.'),
|
||||
},
|
||||
)
|
||||
end_time = fields.String(
|
||||
end_time = fields.Time(
|
||||
required=True,
|
||||
validate=validate.Regexp(r'^\d{2}:\d{2}$', error=_l('End time must be in HH:MM format.')),
|
||||
error_messages={
|
||||
'invalid': _l('End time must be in HH:MM format.'),
|
||||
'required': _l('An end time is required.'),
|
||||
},
|
||||
)
|
||||
points = fields.String(
|
||||
validate=validate.Length(max=2000, error=_l('Points must be 2000 characters or less.')),
|
||||
@@ -490,22 +503,57 @@ class OneOnOneRequestSchema(StripMixin):
|
||||
load_default=None,
|
||||
)
|
||||
|
||||
@validates_schema
|
||||
def validate_window(self, data, **kwargs):
|
||||
"""A session cannot end before it starts.
|
||||
|
||||
class DisponibilityAddSchema(StripMixin):
|
||||
"""Validate disponibility block addition.
|
||||
Nothing checked this. The form's own `<select>`s made it awkward to
|
||||
do by accident, which is not the same as impossible — and the request
|
||||
went to the coach's Discord either way.
|
||||
"""
|
||||
start, end = data.get('start_time'), data.get('end_time')
|
||||
if start and end and end <= start:
|
||||
raise ValidationError(_l('End time must be after start time.'), field_name='end_time')
|
||||
|
||||
Fields:
|
||||
day_of_week: Integer 0-6, required.
|
||||
start_time: Time string (HH:MM), required.
|
||||
|
||||
class TimeSlotSchema(Schema):
|
||||
"""One half-hour slot on a weekday: an availability or a coach booking.
|
||||
|
||||
Replaces `DisponibilityAddSchema`, which was also defined and never
|
||||
called. Used for both the form-encoded single add and the JSON bulk
|
||||
posts, which is why it is a plain Schema — `StripMixin` reads a blank
|
||||
string as an absent field, and these payloads arrive as JSON where a
|
||||
missing key is already absent.
|
||||
|
||||
`day_of_week` was checked inline as `day is None or day < 0 or day > 6`,
|
||||
which raises TypeError rather than rejecting when the JSON holds a
|
||||
string. Here it is an Integer field with a Range.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Marshmallow Meta options.
|
||||
|
||||
EXCLUDE, like StripMixin, because the single-slot endpoint is posted
|
||||
as a form and therefore carries `csrf_token`. Without this the schema
|
||||
rejected every valid submission — found by the test, not by reading.
|
||||
"""
|
||||
|
||||
unknown = EXCLUDE
|
||||
|
||||
day_of_week = fields.Integer(
|
||||
required=True,
|
||||
validate=validate.Range(min=0, max=6, error=_l('Day must be 0 (Monday) to 6 (Sunday).')),
|
||||
error_messages={
|
||||
'invalid': _l('Day must be 0 (Monday) to 6 (Sunday).'),
|
||||
'required': _l('A day is required.'),
|
||||
},
|
||||
)
|
||||
start_time = fields.String(
|
||||
start_time = fields.Time(
|
||||
required=True,
|
||||
validate=validate.Regexp(r'^\d{2}:\d{2}$', error=_l('Start time must be in HH:MM format.')),
|
||||
error_messages={
|
||||
'invalid': _l('Start time must be in HH:MM format.'),
|
||||
'required': _l('A start time is required.'),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user