"""Helpers used by more than one route module in this package. Nothing here touches the blueprint: these are plain functions, so a test can call them with a request context and nothing else. """ from flask import flash, request from flask_babel import gettext as _ from app.extensions import db from app.models import Admin, Coach, Manager, Player, Scout, GAME_PLATFORMS, UserGamertag ALLOWED_CONTRACT_EXTENSIONS = {'pdf'} ALLOWED_SIGNED_EXTENSIONS = {'pdf'} #: Every PDF starts with this. Checking the name alone accepted a file #: called anything.pdf holding anything at all. PDF_SIGNATURE = b'%PDF-' #: USER_TYPE → model class, for create_user. USER_CLASS_MAP = { 'admin': Admin, 'manager': Manager, 'coach': Coach, 'player': Player, 'scout': Scout, } def pdf_upload_error(file, allowed_extensions): """Why this upload is not an acceptable PDF, or None if it is. upload_signed_contract checked nothing beyond a non-empty filename — ALLOWED_SIGNED_EXTENSIONS was declared and never read — so a player could put an arbitrary file on the server under a name the application later hands back for download (SEC-021). Args: file: The uploaded FileStorage, or None. allowed_extensions: Extensions to accept, lowercase and without dot. Returns: str | None: A message to flash, or None when the file is acceptable. """ if file is None or not file.filename: return _('No file selected.') stem, dot, extension = file.filename.rpartition('.') if not (stem and dot) or extension.lower() not in allowed_extensions: return _('Only PDF files are allowed for contracts.') head = file.stream.read(len(PDF_SIGNATURE)) file.stream.seek(0) if head != PDF_SIGNATURE: return _('That file is not a PDF, whatever its name says.') return None def flash_validation_errors(err): """Surface marshmallow errors the same way auth.py already does.""" 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". """ 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 def update_user_gamertags(user, selected_games): """Update gamertags for a user based on form input.""" existing_gamertags = {gt.game: gt for gt in user.gamertags} for game in selected_games: gamertag = request.form.get(f'gamertag_{game}', '').strip() platform = ( request.form.get(f'platform_{game}', '').strip() if GAME_PLATFORMS.get(game) else None ) existing = existing_gamertags.get(game) if gamertag: if existing: existing.gamertag = gamertag existing.platform = platform else: gt = UserGamertag(user_id=user.id, game=game, gamertag=gamertag, platform=platform) db.session.add(gt) elif existing: db.session.delete(existing) for game in existing_gamertags: if game not in selected_games: db.session.delete(existing_gamertags[game])