Files
team-tryouts/app/routes/users/contracts.py
GGThed e15b3c1293
CI - Security, Lint & Tests / validate (push) Failing after 16m22s
fix(audit): centraliser l'horloge UTC
2026-08-17 15:21:08 -04:00

212 lines
7.8 KiB
Python

"""Player contracts: upload, sign, download."""
import os
import uuid
from flask import flash, redirect, render_template, request, send_file, url_for
from flask_babel import gettext as _
from flask_login import current_user, login_required
from marshmallow import ValidationError
from werkzeug.utils import secure_filename
from app.extensions import db
from app.models import Admin, Coach, Contract, Manager, Player, User
from app.permissions import can_manage_player_contract, coach_player_ids
from app.routes.users._shared import (
ALLOWED_CONTRACT_EXTENSIONS,
ALLOWED_SIGNED_EXTENSIONS,
pdf_upload_error,
)
from app.routes.users.blueprint import users_bp
from app.storage import CONTRACTS_DIR, document_path
from app.time_utils import utc_now_naive
from app.validators import UploadContractSchema
def manageable_players():
"""Players the current user may attach a contract to.
A coach used to see the squad of one team — the first row matching the
legacy coach_id column — so a coach of two teams could file a contract
for half of their players and no more, and a coach attached only by the
many-to-many relationship for none at all.
"""
if isinstance(current_user, Coach):
player_ids = coach_player_ids(current_user)
return (
User.query.filter(User.id.in_(player_ids)).order_by(User.username).all()
if player_ids
else []
)
# is_active_account: a contract select that still lists people who have
# left the club invites filing paperwork against them (SEC-16).
return User.query.filter_by(role='player', is_active_account=True).order_by(User.username).all()
@users_bp.route('/contracts')
@login_required
def list_contracts():
"""View contracts for the current user or players they manage."""
contracts = None
players = None
if isinstance(current_user, Player):
contracts = (
Contract.query.filter_by(
player_id=current_user.id,
)
.order_by(Contract.uploaded_at.desc())
.all()
)
elif isinstance(current_user, (Admin, Manager, Coach)):
players = manageable_players()
if players:
player_ids = [p.id for p in players]
contracts = (
Contract.query.filter(
Contract.player_id.in_(player_ids),
)
.order_by(Contract.uploaded_at.desc())
.all()
)
return render_template(
'pages/contracts.html',
contracts=contracts,
players=players if isinstance(current_user, (Admin, Manager, Coach)) else None,
)
@users_bp.route('/contracts/upload', methods=['GET', 'POST'])
@login_required
def upload_contract():
"""Upload a contract for a player."""
if not isinstance(current_user, (Admin, Manager, Coach)):
flash(_('Only presidents, managers, and coaches can upload contracts.'), 'danger')
return redirect(url_for('users.list_contracts'))
players = manageable_players()
if request.method == 'POST':
contract_schema = UploadContractSchema()
try:
validated = contract_schema.load(request.form)
except ValidationError as err:
for field, messages in err.messages.items():
for msg in messages:
flash(_('%(field)s: %(msg)s', field=field, msg=msg), 'danger')
return render_template('pages/upload_contract.html', players=players)
player_id = validated['player_id']
notes = validated.get('notes')
if not can_manage_player_contract(current_user, player_id):
flash(_('You do not have permission to upload a contract for this player.'), 'danger')
return redirect(url_for('users.upload_contract'))
file = request.files.get('contract_file')
error = pdf_upload_error(file, ALLOWED_CONTRACT_EXTENSIONS)
if error:
flash(error, 'danger')
return redirect(url_for('users.upload_contract'))
player = db.get_or_404(User, player_id)
player_teams = player.get_org_teams()
team = player_teams[0] if player_teams else None
original_filename = secure_filename(file.filename)
stored_filename = f"{uuid.uuid4()}.pdf"
# Kept relative to the document root, not absolute (see app/storage.py):
# an absolute path pins the file to the directory the process was
# started from, which is the one thing a release-directory deploy
# changes.
relative_path = os.path.join(CONTRACTS_DIR, stored_filename)
if team:
relative_path = os.path.join(CONTRACTS_DIR, secure_filename(team.name), stored_filename)
absolute_path = document_path(relative_path)
os.makedirs(os.path.dirname(absolute_path), exist_ok=True)
file.save(absolute_path)
contract = Contract(
player_id=player_id,
team_id=team.id if team else None,
uploaded_by_id=current_user.id,
original_filename=original_filename,
stored_filename=stored_filename,
file_path=relative_path,
notes=notes if notes else None,
)
db.session.add(contract)
db.session.commit()
flash(
_('Contract uploaded successfully for %(username)s!', username=player.username),
'success',
)
return redirect(url_for('users.list_contracts'))
return render_template('pages/upload_contract.html', players=players)
@users_bp.route('/contracts/<int:contract_id>/upload_signed', methods=['POST'])
@login_required
def upload_signed_contract(contract_id):
"""Upload a signed contract (player only)."""
contract = db.get_or_404(Contract, contract_id)
if not contract.can_upload_signed(current_user):
flash(_('Only the player can upload their signed contract.'), 'danger')
return redirect(url_for('users.list_contracts'))
file = request.files.get('signed_file')
error = pdf_upload_error(file, ALLOWED_SIGNED_EXTENSIONS)
if error:
flash(error, 'danger')
return redirect(url_for('users.list_contracts'))
signed_filename = f"signed_{contract.stored_filename}"
signed_path = contract.file_path.replace(contract.stored_filename, signed_filename)
file.save(document_path(signed_path))
contract.signed_filename = signed_filename
contract.signed_file_path = signed_path
contract.status = 'signed'
contract.signed_at = utc_now_naive()
db.session.commit()
flash(_('Signed contract uploaded successfully!'), 'success')
return redirect(url_for('users.list_contracts'))
@users_bp.route('/contracts/<int:contract_id>/download')
@login_required
def download_contract(contract_id):
"""Download a contract file."""
contract = db.get_or_404(Contract, contract_id)
if not contract.can_view(current_user):
flash(_('You do not have permission to download this contract.'), 'danger')
return redirect(url_for('users.list_contracts'))
return send_file(
document_path(contract.file_path),
as_attachment=True,
download_name=contract.original_filename,
)
@users_bp.route('/contracts/<int:contract_id>/download_signed')
@login_required
def download_signed_contract(contract_id):
"""Download a signed contract file."""
contract = db.get_or_404(Contract, contract_id)
if not contract.can_view(current_user):
flash(_('You do not have permission to download this contract.'), 'danger')
return redirect(url_for('users.list_contracts'))
if not contract.signed_file_path:
flash(_('No signed contract available.'), 'danger')
return redirect(url_for('users.list_contracts'))
return send_file(
document_path(contract.signed_file_path),
as_attachment=True,
download_name=contract.signed_filename,
)