premiere iteration de la gestion des documents
CI - Security, Lint & Tests / validate (push) Failing after 1m20s

This commit is contained in:
cedrick2711
2026-09-11 09:51:48 -04:00
parent bd9ed4bdc7
commit abf2f06642
14 changed files with 1326 additions and 5 deletions
+189 -1
View File
@@ -6,6 +6,7 @@ to administrators.
"""
import os
import uuid
from datetime import datetime
from flask import (
@@ -17,14 +18,23 @@ from flask import (
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.forms import flash_validation_errors
from app.models import (
DOCUMENT_KIND_ROLES,
DOCUMENT_KINDS,
Admin,
AppSettings,
AuditLog,
BackupRecord,
Document,
DocumentAssignment,
DocumentVersion,
OrgTeam,
TeamMatch,
TeamMatchParticipant,
@@ -33,12 +43,16 @@ from app.models import (
User,
)
from app.responders import respond_after_change
from app.routes.users._shared import ALLOWED_CONTRACT_EXTENSIONS, pdf_upload_error
from app.services.documents import backfill_assignments_for_document
from app.storage import GoogleDriveStorageError, open_document, store_master_document
from app.supporting_scripts.backup import (
BackupError,
backup_database,
create_backup_dir,
parse_database_url,
)
from app.validators import DocumentCreateSchema
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
@@ -211,7 +225,7 @@ def restore_backup(backup_id):
# Create a safety backup of the current state before restoring.
try:
safety = _create_backup_record(
_create_backup_record(
backup_type='pre_restore',
notes='Automatic safety backup before restoring ' + record.filename,
)
@@ -423,3 +437,177 @@ def wipe_teams():
'success',
)
return respond_after_change(url_for('admin.dashboard'))
# ---------------------------------------------------------------------------
# Documents (canonical document store)
# ---------------------------------------------------------------------------
@admin_bp.route('/documents')
@login_required
def documents():
"""List every document with its published flag and completion counts."""
if not require_admin():
return redirect(url_for('main.dashboard'))
documents = Document.query.order_by(Document.kind, Document.title).all()
return render_template(
'pages/admin_documents.html',
documents=documents,
kinds=DOCUMENT_KINDS,
kind_roles=DOCUMENT_KIND_ROLES,
)
@admin_bp.route('/documents/create', methods=['POST'])
@login_required
def create_document():
"""Create a new canonical document (no file yet; publish one next)."""
if not require_admin():
return redirect(url_for('main.dashboard'))
try:
data = DocumentCreateSchema().load(request.form)
except ValidationError as err:
flash_validation_errors(err)
return respond_after_change(url_for('admin.documents'))
document = Document(
title=data['title'],
kind=data['kind'],
created_by=current_user.id,
)
db.session.add(document)
db.session.commit()
_log('document_created', f'{data["kind"]}: {data["title"]}')
flash(_('Document "%(title)s" created.', title=data['title']), 'success')
return respond_after_change(url_for('admin.documents'))
@admin_bp.route('/documents/<int:doc_id>/publish-version', methods=['POST'])
@login_required
def publish_document_version(doc_id):
"""Store a new PDF once and record it as the current version."""
if not require_admin():
return redirect(url_for('main.dashboard'))
document = db.get_or_404(Document, doc_id)
file = request.files.get('version_file')
error = pdf_upload_error(file, ALLOWED_CONTRACT_EXTENSIONS)
if error:
flash(error, 'danger')
return respond_after_change(url_for('admin.document_detail', doc_id=document.id))
original_filename = secure_filename(file.filename)
stored_filename = f'{uuid.uuid4()}.pdf'
try:
stored_path = store_master_document(file, stored_filename)
except GoogleDriveStorageError:
flash(
_('Document storage is temporarily unavailable. Please try again later.'),
'danger',
)
return respond_after_change(url_for('admin.document_detail', doc_id=document.id))
version = DocumentVersion(
document_id=document.id,
version=document.next_version_number(),
original_filename=original_filename,
stored_filename=stored_filename,
file_path=stored_path,
notes=request.form.get('notes', '').strip() or None,
published_by=current_user.id,
)
db.session.add(version)
db.session.flush()
document.current_version_id = version.id
db.session.commit()
backfill_assignments_for_document(document)
_log('document_version_published', f'{document.title} v{version.version}')
flash(
_(
'Version %(number)s of "%(title)s" published.',
number=version.version,
title=document.title,
),
'success',
)
return respond_after_change(url_for('admin.document_detail', doc_id=document.id))
@admin_bp.route('/documents/<int:doc_id>/toggle-published', methods=['POST'])
@login_required
def toggle_document_published(doc_id):
"""Flip whether team members can see and download the document."""
if not require_admin():
return redirect(url_for('main.dashboard'))
document = db.get_or_404(Document, doc_id)
document.is_published = not document.is_published
db.session.commit()
if document.is_published:
# Enabling a document must also backfill anyone who is nowadays
# eligible but was not when versions were published earlier.
created = backfill_assignments_for_document(document)
_log('document_published', f'{document.title} (assignments: {created})')
flash(
_('"%(title)s" is now downloadable by team members.', title=document.title),
'success',
)
else:
_log('document_unpublished', document.title)
flash(
_('"%(title)s" is no longer downloadable.', title=document.title),
'success',
)
return respond_after_change(url_for('admin.documents'))
@admin_bp.route('/documents/version/<int:version_id>/download')
@login_required
def download_document_version(version_id):
"""Admin download of a stored document revision."""
if not require_admin():
return redirect(url_for('main.dashboard'))
version = db.get_or_404(DocumentVersion, version_id)
try:
document = open_document(version.file_path)
except GoogleDriveStorageError:
flash(
_('Document storage is temporarily unavailable. Please try again later.'),
'danger',
)
return respond_after_change(url_for('admin.documents'))
return send_file(document, as_attachment=True, download_name=version.original_filename)
@admin_bp.route('/documents/<int:doc_id>')
@login_required
def document_detail(doc_id):
"""Revisions and per-person completion for one document."""
if not require_admin():
return redirect(url_for('main.dashboard'))
document = db.get_or_404(Document, doc_id)
versions = document.versions.order_by(DocumentVersion.version).all()
assignments = (
db.session.query(DocumentAssignment, User)
.join(User, User.id == DocumentAssignment.user_id)
.filter(DocumentAssignment.document_id == document.id)
.order_by(User.username)
.all()
)
return render_template(
'pages/admin_document_detail.html',
document=document,
versions=versions,
assignments=assignments,
kinds=DOCUMENT_KINDS,
)