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('/') @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('//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('//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('//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('//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('//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})