"""The boundary between an HTTP form and a validated payload (ARCH-005). Every POST in this application arrives as a `werkzeug.MultiDict` of strings. Turning that into typed, checked values was done inline, differently, in each route: `int(x) if x else None` here, `datetime.strptime` inside a bare `try` there, and in several places not at all. The failures that produced were not loud ones — a bad time silently became `None` and the page said the match had been updated. Two functions here, one schema module next to them (`app.validators`): payload = form_payload(list_fields=('player_ids',)) try: data = MatchSchema().load(payload) except ValidationError as err: flash_validation_errors(err) return _rerender() Both were originally inside `app/routes/users/_shared.py`, which is where they were first needed. They are re-exported from there so that nothing had to be renamed when the match and tryout routes started using them too. """ from flask import flash, request from flask_babel import gettext as _ def flash_validation_errors(err): """Surface marshmallow errors, one flash per problem. The uniform reporting half of ARCH-005: before this, a bad date flashed 'Invalid date format.' from one route, redirected from another, and was silently dropped by a third. """ for field, messages in err.messages.items(): for msg in messages: flash(_('%(field)s: %(msg)s', field=field, msg=msg), 'danger') def form_payload(*, checkboxes=(), list_fields=('games',), optional_blank=('password',)): """Turn the multi-valued request form into a plain dict for marshmallow. request.form.to_dict() keeps only the first value of a repeated key, so list fields have to be re-read with getlist(). Unchecked HTML checkboxes are simply absent from the submission, which is not the same as a schema default, so they are injected explicitly. Blank optional fields are dropped rather than sent as '' — an empty password means "leave the current one alone", not "set the password to the empty string". Args: checkboxes: Names to report as True/False on presence. list_fields: Names to read with getlist(), always producing a list. optional_blank: Names to drop entirely when submitted empty. """ payload = request.form.to_dict() for name in list_fields: payload[name] = request.form.getlist(name) for name in checkboxes: payload[name] = name in request.form for name in optional_blank: if not payload.get(name): payload.pop(name, None) return payload