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:
+189
-1
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user