212 lines
7.8 KiB
Python
212 lines
7.8 KiB
Python
"""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 = 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 = datetime.utcnow()
|
|
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,
|
|
)
|