Initial commit: add compliance_checks table, per-check metadata on assets, and compliance audit trail
This commit is contained in:
202
app/routes/doc_templates.py
Normal file
202
app/routes/doc_templates.py
Normal file
@@ -0,0 +1,202 @@
|
||||
import json
|
||||
import os
|
||||
from flask import (Blueprint, render_template, redirect, url_for,
|
||||
flash, request, current_app, send_from_directory, jsonify)
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug.utils import secure_filename
|
||||
from app.extensions import db
|
||||
from app.models.document_template import DocumentTemplate
|
||||
from app.models.paperwork import DOC_TYPES
|
||||
from app.models.audit_log import AuditLog
|
||||
from app.services.template_service import extract_variables
|
||||
|
||||
bp = Blueprint('doc_templates', __name__, url_prefix='/doc-templates')
|
||||
|
||||
ALLOWED_EXT = {'docx'}
|
||||
|
||||
|
||||
def _log(action, record_id, description, new=None):
|
||||
entry = AuditLog(
|
||||
table_name='document_templates',
|
||||
record_id=record_id,
|
||||
action=action,
|
||||
new_values=json.dumps(new) if new else None,
|
||||
performed_by_id=current_user.id,
|
||||
ip_address=request.remote_addr,
|
||||
description=description,
|
||||
)
|
||||
db.session.add(entry)
|
||||
|
||||
|
||||
def _template_folder(app):
|
||||
folder = os.path.join(app.root_path, '..', app.config.get('TEMPLATE_FOLDER', 'doc_templates'))
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
return folder
|
||||
|
||||
|
||||
def _allowed(filename):
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXT
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# List
|
||||
# ------------------------------------------------------------------
|
||||
@bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
templates = DocumentTemplate.query.order_by(DocumentTemplate.name).all()
|
||||
return render_template('doc_templates/index.html',
|
||||
templates=templates, doc_types=DOC_TYPES)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Upload
|
||||
# ------------------------------------------------------------------
|
||||
@bp.route('/upload', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def upload():
|
||||
if request.method == 'POST':
|
||||
name = request.form.get('name', '').strip()
|
||||
description = request.form.get('description', '').strip() or None
|
||||
category = request.form.get('category', '') or None
|
||||
f = request.files.get('docx_file')
|
||||
|
||||
if not name:
|
||||
flash('Template name is required.', 'danger')
|
||||
return render_template('doc_templates/upload.html', doc_types=DOC_TYPES)
|
||||
|
||||
if not f or not f.filename:
|
||||
flash('Please select a .docx file.', 'danger')
|
||||
return render_template('doc_templates/upload.html', doc_types=DOC_TYPES)
|
||||
|
||||
if not _allowed(f.filename):
|
||||
flash('Only .docx files are accepted.', 'danger')
|
||||
return render_template('doc_templates/upload.html', doc_types=DOC_TYPES)
|
||||
|
||||
folder = _template_folder(current_app)
|
||||
safe_name = secure_filename(f.filename)
|
||||
# Prefix with timestamp to avoid collisions
|
||||
from datetime import datetime as _dt
|
||||
prefix = _dt.utcnow().strftime('%Y%m%d_%H%M%S_')
|
||||
filename = prefix + safe_name
|
||||
save_path = os.path.join(folder, filename)
|
||||
f.save(save_path)
|
||||
|
||||
# Extract variables from the uploaded template
|
||||
try:
|
||||
variables = extract_variables(save_path)
|
||||
except Exception as exc:
|
||||
current_app.logger.warning('Variable extraction failed: %s', exc)
|
||||
variables = []
|
||||
|
||||
tpl = DocumentTemplate(
|
||||
name=name,
|
||||
description=description,
|
||||
category=category,
|
||||
filename=filename,
|
||||
created_by_id=current_user.id,
|
||||
)
|
||||
tpl.variables = variables
|
||||
db.session.add(tpl)
|
||||
db.session.flush()
|
||||
_log('create', tpl.id, f'Uploaded template "{name}"', new={'name': name, 'filename': filename})
|
||||
db.session.commit()
|
||||
flash(f'Template "{name}" uploaded. Found {len(variables)} variable(s).', 'success')
|
||||
return redirect(url_for('doc_templates.detail', tpl_id=tpl.id))
|
||||
|
||||
return render_template('doc_templates/upload.html', doc_types=DOC_TYPES)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Detail / variable list
|
||||
# ------------------------------------------------------------------
|
||||
@bp.route('/<int:tpl_id>')
|
||||
@login_required
|
||||
def detail(tpl_id):
|
||||
tpl = DocumentTemplate.query.get_or_404(tpl_id)
|
||||
return render_template('doc_templates/detail.html', tpl=tpl, doc_types=DOC_TYPES)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Download original template file
|
||||
# ------------------------------------------------------------------
|
||||
@bp.route('/<int:tpl_id>/download')
|
||||
@login_required
|
||||
def download(tpl_id):
|
||||
tpl = DocumentTemplate.query.get_or_404(tpl_id)
|
||||
folder = _template_folder(current_app)
|
||||
return send_from_directory(folder, tpl.filename, as_attachment=True,
|
||||
download_name=tpl.name + '.docx')
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Re-scan variables (after template is replaced / edited externally)
|
||||
# ------------------------------------------------------------------
|
||||
@bp.route('/<int:tpl_id>/rescan', methods=['POST'])
|
||||
@login_required
|
||||
def rescan(tpl_id):
|
||||
tpl = DocumentTemplate.query.get_or_404(tpl_id)
|
||||
folder = _template_folder(current_app)
|
||||
path = os.path.join(folder, tpl.filename)
|
||||
try:
|
||||
variables = extract_variables(path)
|
||||
tpl.variables = variables
|
||||
db.session.commit()
|
||||
flash(f'Rescanned: found {len(variables)} variable(s).', 'success')
|
||||
except Exception as exc:
|
||||
flash(f'Rescan failed: {exc}', 'danger')
|
||||
return redirect(url_for('doc_templates.detail', tpl_id=tpl_id))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Edit metadata
|
||||
# ------------------------------------------------------------------
|
||||
@bp.route('/<int:tpl_id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit(tpl_id):
|
||||
tpl = DocumentTemplate.query.get_or_404(tpl_id)
|
||||
if request.method == 'POST':
|
||||
tpl.name = request.form.get('name', tpl.name).strip()
|
||||
tpl.description = request.form.get('description', '').strip() or None
|
||||
tpl.category = request.form.get('category', '') or None
|
||||
db.session.commit()
|
||||
flash('Template updated.', 'success')
|
||||
return redirect(url_for('doc_templates.detail', tpl_id=tpl_id))
|
||||
return render_template('doc_templates/edit.html', tpl=tpl, doc_types=DOC_TYPES)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Delete
|
||||
# ------------------------------------------------------------------
|
||||
@bp.route('/<int:tpl_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete(tpl_id):
|
||||
tpl = DocumentTemplate.query.get_or_404(tpl_id)
|
||||
# Check if any documents were generated from this template
|
||||
if tpl.paperwork_docs.count() > 0:
|
||||
flash(f'Cannot delete — {tpl.paperwork_docs.count()} document(s) were generated from this template.', 'danger')
|
||||
return redirect(url_for('doc_templates.detail', tpl_id=tpl_id))
|
||||
|
||||
folder = _template_folder(current_app)
|
||||
file_path = os.path.join(folder, tpl.filename)
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
_log('delete', tpl.id, f'Deleted template "{tpl.name}"')
|
||||
db.session.delete(tpl)
|
||||
db.session.commit()
|
||||
flash(f'Template "{tpl.name}" deleted.', 'success')
|
||||
return redirect(url_for('doc_templates.index'))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# AJAX: return variables for a template (used by paperwork form)
|
||||
# ------------------------------------------------------------------
|
||||
@bp.route('/<int:tpl_id>/variables.json')
|
||||
@login_required
|
||||
def variables_json(tpl_id):
|
||||
tpl = DocumentTemplate.query.get_or_404(tpl_id)
|
||||
return jsonify({'variables': tpl.variables, 'name': tpl.name})
|
||||
Reference in New Issue
Block a user