"""Player contracts: upload, sign, download.""" import os import uuid from datetime import datetime 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.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 = User.query.get_or_404(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//upload_signed', methods=['POST']) @login_required def upload_signed_contract(contract_id): """Upload a signed contract (player only).""" contract = Contract.query.get_or_404(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 = datetime.utcnow() db.session.commit() flash(_('Signed contract uploaded successfully!'), 'success') return redirect(url_for('users.list_contracts')) @users_bp.route('/contracts//download') @login_required def download_contract(contract_id): """Download a contract file.""" contract = Contract.query.get_or_404(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//download_signed') @login_required def download_signed_contract(contract_id): """Download a signed contract file.""" contract = Contract.query.get_or_404(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, )