Active la regle isort (I) de ruff. 45 fichiers reordonnes, aucun changement de comportement : la suite passe avant comme apres. app/models/__init__.py en est exclu. Ses imports sont ranges en onze couches commentees qui decrivent le graphe de dependances ; trier par ordre alphabetique laisse chaque titre au-dessus d un import qu il ne decrit pas, et ce fichier n a qu un role, etre lu. Commit isole, comme le formatage : un diff de brassage ne doit pas servir de couverture a un changement de comportement.
109 lines
3.9 KiB
Python
109 lines
3.9 KiB
Python
"""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 GAME_PLATFORMS, Admin, Coach, Manager, Player, Scout, 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])
|