premiere iteration de la gestion des documents
CI - Security, Lint & Tests / validate (push) Failing after 1m20s
CI - Security, Lint & Tests / validate (push) Failing after 1m20s
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
"""Member-facing document centre.
|
||||
|
||||
Players, coaches and managers see the published documents they must sign
|
||||
or acknowledge, complete them, and download the file drawn from the single
|
||||
canonical copy in storage. Coaches and managers additionally see progress
|
||||
for the members of the teams they are attached to.
|
||||
"""
|
||||
|
||||
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 app.extensions import db
|
||||
from app.models import (
|
||||
DOCUMENT_KIND_ROLES,
|
||||
Admin,
|
||||
Coach,
|
||||
Document,
|
||||
DocumentAssignment,
|
||||
DocumentVersion,
|
||||
Manager,
|
||||
)
|
||||
from app.permissions import visible_org_teams
|
||||
from app.responders import respond_after_change
|
||||
from app.routes.users._shared import ALLOWED_SIGNED_EXTENSIONS, pdf_upload_error
|
||||
from app.routes.users.blueprint import users_bp
|
||||
from app.services.documents import (
|
||||
backfill_assignments_for_user,
|
||||
primary_team_for_user,
|
||||
user_eligible_for_kind,
|
||||
user_has_any_team,
|
||||
)
|
||||
from app.storage import GoogleDriveStorageError, open_document, store_signed_document
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
def _team_members(team):
|
||||
"""Active players, coaches and managers of one team, de-duplicated."""
|
||||
members = {}
|
||||
for player in team.players:
|
||||
if player.is_active_account:
|
||||
members[player.id] = player
|
||||
for staff in list(team.get_coaches()) + list(team.get_managers()):
|
||||
if staff.is_active_account:
|
||||
members[staff.id] = staff
|
||||
return sorted(members.values(), key=lambda user: user.username.lower())
|
||||
|
||||
|
||||
@users_bp.route('/documents')
|
||||
@login_required
|
||||
def list_my_documents():
|
||||
"""The current member's obligations, plus team progress for staff."""
|
||||
backfill_assignments_for_user(current_user)
|
||||
|
||||
if isinstance(current_user, Admin):
|
||||
return redirect(url_for('admin.documents'))
|
||||
|
||||
if not user_has_any_team(current_user):
|
||||
return render_template('pages/my_documents.html', documents=[], teams_progress=None)
|
||||
|
||||
published = (
|
||||
Document.query.filter(Document.is_published.is_(True))
|
||||
.order_by(Document.kind, Document.title)
|
||||
.all()
|
||||
)
|
||||
|
||||
documents = []
|
||||
for doc in published:
|
||||
if current_user.role not in DOCUMENT_KIND_ROLES.get(doc.kind, ()):
|
||||
continue
|
||||
assignment = DocumentAssignment.query.filter_by(
|
||||
document_id=doc.id, user_id=current_user.id
|
||||
).first()
|
||||
documents.append({'document': doc, 'assignment': assignment})
|
||||
|
||||
teams_progress = None
|
||||
if isinstance(current_user, (Coach, Manager)):
|
||||
teams_progress = []
|
||||
for team in visible_org_teams(current_user):
|
||||
rows = []
|
||||
for member in _team_members(team):
|
||||
statuses = {}
|
||||
for doc in published:
|
||||
if member.role not in DOCUMENT_KIND_ROLES.get(doc.kind, ()):
|
||||
continue
|
||||
assignment = DocumentAssignment.query.filter_by(
|
||||
document_id=doc.id, user_id=member.id
|
||||
).first()
|
||||
statuses[doc.id] = assignment.status if assignment else None
|
||||
rows.append({'user': member, 'statuses': statuses})
|
||||
teams_progress.append({'team': team, 'members': rows, 'documents': published})
|
||||
|
||||
return render_template(
|
||||
'pages/my_documents.html',
|
||||
documents=documents,
|
||||
teams_progress=teams_progress,
|
||||
)
|
||||
|
||||
|
||||
@users_bp.route('/documents/assignments/<int:assignment_id>/sign', methods=['POST'])
|
||||
@login_required
|
||||
def sign_document(assignment_id):
|
||||
"""Upload the signed copy of a pending signature document."""
|
||||
assignment = db.get_or_404(DocumentAssignment, assignment_id)
|
||||
if assignment.user_id != current_user.id:
|
||||
flash(_('You do not have permission to sign this document.'), 'danger')
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
|
||||
if assignment.completed:
|
||||
flash(_('This document has already been completed.'), 'danger')
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
if not assignment.document.requires_signature:
|
||||
flash(_('This document is acknowledged, not signed.'), 'danger')
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
if not assignment.document.current_version:
|
||||
flash(_('This document has no version published yet.'), 'danger')
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
|
||||
file = request.files.get('signed_file')
|
||||
error = pdf_upload_error(file, ALLOWED_SIGNED_EXTENSIONS)
|
||||
if error:
|
||||
flash(error, 'danger')
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
|
||||
team = primary_team_for_user(current_user)
|
||||
if team is None:
|
||||
flash(_('You must be part of a team before signing documents.'), 'danger')
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
|
||||
version = assignment.document.current_version
|
||||
# The kind prefix keeps separate documents from colliding: a player signs
|
||||
# both the player contract and the team agreement, each at version 1.
|
||||
signed_filename = f'{assignment.document.kind}-v{version.version}.pdf'
|
||||
try:
|
||||
stored_path = store_signed_document(
|
||||
file,
|
||||
team_name=team.name,
|
||||
role=current_user.role,
|
||||
username=current_user.username,
|
||||
filename=signed_filename,
|
||||
)
|
||||
except GoogleDriveStorageError:
|
||||
flash(
|
||||
_('Document storage is temporarily unavailable. Please try again later.'),
|
||||
'danger',
|
||||
)
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
|
||||
assignment.status = 'signed'
|
||||
assignment.signed_filename = signed_filename
|
||||
assignment.signed_file_path = stored_path
|
||||
assignment.completed_version_id = version.id
|
||||
assignment.completed_at = utc_now_naive()
|
||||
assignment.filed_team_id = team.id
|
||||
db.session.commit()
|
||||
|
||||
flash(_('Document signed successfully!'), 'success')
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
|
||||
|
||||
@users_bp.route('/documents/assignments/<int:assignment_id>/acknowledge', methods=['POST'])
|
||||
@login_required
|
||||
def acknowledge_document(assignment_id):
|
||||
"""Mark a read-and-acknowledge document (code of conduct, ethics)."""
|
||||
assignment = db.get_or_404(DocumentAssignment, assignment_id)
|
||||
if assignment.user_id != current_user.id:
|
||||
flash(_('You do not have permission to acknowledge this document.'), 'danger')
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
|
||||
if assignment.completed:
|
||||
flash(_('This document has already been completed.'), 'danger')
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
if assignment.document.requires_signature:
|
||||
flash(_('This document must be signed, not acknowledged.'), 'danger')
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
if not assignment.document.current_version:
|
||||
flash(_('This document has no version published yet.'), 'danger')
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
|
||||
assignment.status = 'acknowledged'
|
||||
assignment.completed_version_id = assignment.document.current_version.id
|
||||
assignment.completed_at = utc_now_naive()
|
||||
db.session.commit()
|
||||
|
||||
flash(_('Document acknowledged.'), 'success')
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
|
||||
|
||||
@users_bp.route('/documents/version/<int:version_id>/download')
|
||||
@login_required
|
||||
def download_document(version_id):
|
||||
"""Download the canonical file, gated on the admin's published flag."""
|
||||
version = db.get_or_404(DocumentVersion, version_id)
|
||||
allowed = isinstance(current_user, Admin) or (
|
||||
version.document.is_published
|
||||
and user_eligible_for_kind(current_user, version.document.kind)
|
||||
)
|
||||
if not allowed:
|
||||
flash(_('This document is not available to you.'), 'danger')
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
|
||||
try:
|
||||
file_object = open_document(version.file_path)
|
||||
except GoogleDriveStorageError:
|
||||
flash(
|
||||
_('Document storage is temporarily unavailable. Please try again later.'),
|
||||
'danger',
|
||||
)
|
||||
return respond_after_change(url_for('users.list_my_documents'))
|
||||
return send_file(file_object, as_attachment=True, download_name=version.original_filename)
|
||||
Reference in New Issue
Block a user