Phase 2: implement template form editor

This commit is contained in:
2026-02-20 12:57:44 -05:00
parent 225ec9000a
commit b62df442b5
8 changed files with 2419 additions and 284 deletions
+221 -43
View File
@@ -1,24 +1,32 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user
from flask_wtf.csrf import generate_csrf
from app import db
from app.models.inspection import InspectionTemplate, ChecklistItem
from app.utils.forms import InspectionTemplateForm, ChecklistItemForm
from app.utils.decorators import supervisor_required
import json
bp = Blueprint('templates', __name__, url_prefix='/templates')
# ---------------------------------------------------------------------------
# Template CRUD
# ---------------------------------------------------------------------------
@bp.route('/')
@login_required
def index():
templates = InspectionTemplate.query.order_by(InspectionTemplate.name).all()
return render_template('templates/list.html', templates=templates)
@bp.route('/new', methods=['GET', 'POST'])
@login_required
@supervisor_required
def create_template():
form = InspectionTemplateForm()
if form.validate_on_submit():
template = InspectionTemplate(
name=form.name.data,
@@ -26,30 +34,26 @@ def create_template():
frequency=form.frequency.data,
created_by=current_user.id
)
db.session.add(template)
db.session.commit()
flash(f'Template "{template.name}" created successfully.', 'success')
return redirect(url_for('templates.edit_template', template_id=template.id))
return redirect(url_for('templates.form_editor', template_id=template.id))
return render_template('templates/form.html', form=form, title='Create Inspection Template')
@bp.route('/<int:template_id>')
@login_required
def view_template(template_id):
template = InspectionTemplate.query.get_or_404(template_id)
checklist_items = template.checklist_items.order_by(ChecklistItem.display_order, ChecklistItem.category).all()
# Group items by category
items_by_category = {}
for item in checklist_items:
category = item.category or 'General'
if category not in items_by_category:
items_by_category[category] = []
items_by_category[category].append(item)
return render_template('templates/view.html', template=template, items_by_category=items_by_category)
form_fields = template.get_form_schema()
return render_template(
'templates/view.html',
template=template,
form_fields=form_fields
)
@bp.route('/<int:template_id>/edit', methods=['GET', 'POST'])
@login_required
@@ -57,39 +61,204 @@ def view_template(template_id):
def edit_template(template_id):
template = InspectionTemplate.query.get_or_404(template_id)
form = InspectionTemplateForm(obj=template)
if form.validate_on_submit():
template.name = form.name.data
template.description = form.description.data
template.frequency = form.frequency.data
db.session.commit()
flash(f'Template "{template.name}" updated successfully.', 'success')
return redirect(url_for('templates.view_template', template_id=template.id))
checklist_items = template.checklist_items.order_by(ChecklistItem.display_order, ChecklistItem.category).all()
return render_template('templates/edit.html', form=form, template=template, checklist_items=checklist_items)
form_fields = template.get_form_schema()
return render_template(
'templates/edit.html',
form=form,
template=template,
form_fields=form_fields
)
@bp.route('/<int:template_id>/rename', methods=['POST'])
@login_required
@supervisor_required
def rename_template(template_id):
template = InspectionTemplate.query.get_or_404(template_id)
new_name = request.form.get('name', '').strip()
if not new_name:
flash('Template name cannot be empty.', 'danger')
return redirect(url_for('templates.index'))
if len(new_name) > 255:
flash('Template name is too long (max 255 characters).', 'danger')
return redirect(url_for('templates.index'))
valid_frequencies = {'daily', 'weekly', 'monthly', 'quarterly'}
new_frequency = request.form.get('frequency', '').strip()
if new_frequency not in valid_frequencies:
flash('Invalid frequency value.', 'danger')
return redirect(url_for('templates.index'))
template.name = new_name
template.description = request.form.get('description', '').strip() or None
template.frequency = new_frequency
db.session.commit()
flash(f'Template "{template.name}" updated successfully.', 'success')
return redirect(url_for('templates.index'))
@bp.route('/<int:template_id>/delete', methods=['POST'])
@login_required
@supervisor_required
def delete_template(template_id):
template = InspectionTemplate.query.get_or_404(template_id)
# Check if template has inspections
if template.inspections.count() > 0:
flash('Cannot delete template with existing inspections.', 'danger')
return redirect(url_for('templates.view_template', template_id=template.id))
return redirect(url_for('templates.index'))
template_name = template.name
db.session.delete(template)
db.session.commit()
flash(f'Template "{template_name}" deleted successfully.', 'success')
return redirect(url_for('templates.index'))
# Checklist Item Management
@bp.route('/<int:template_id>/duplicate', methods=['POST'])
@login_required
@supervisor_required
def duplicate_template(template_id):
src = InspectionTemplate.query.get_or_404(template_id)
# Duplicate the template header
new_tpl = InspectionTemplate(
name=f'{src.name} (Copy)',
description=src.description,
frequency=src.frequency,
created_by=current_user.id
)
db.session.add(new_tpl)
db.session.flush() # get new_tpl.id before committing
# Duplicate all checklist items
for item in src.checklist_items.order_by(ChecklistItem.display_order).all():
new_item = ChecklistItem(
template_id=new_tpl.id,
category=item.category,
item_description=item.item_description,
scoring_type=item.scoring_type,
weight=item.weight,
requires_photo=item.requires_photo,
display_order=item.display_order
)
db.session.add(new_item)
# Duplicate form schema if present
if src.form_schema:
new_tpl.form_schema = src.form_schema
db.session.commit()
flash(f'Template "{src.name}" duplicated successfully.', 'success')
return redirect(url_for('templates.index'))
# ---------------------------------------------------------------------------
# Form Editor
# ---------------------------------------------------------------------------
@bp.route('/<int:template_id>/form-editor')
@login_required
@supervisor_required
def form_editor(template_id):
template = InspectionTemplate.query.get_or_404(template_id)
form_schema = template.get_form_schema()
return render_template(
'templates/form_editor.html',
template=template,
form_schema_json=json.dumps(form_schema),
csrf_token=generate_csrf()
)
@bp.route('/<int:template_id>/form-editor/save', methods=['POST'])
@login_required
@supervisor_required
def save_form_schema(template_id):
"""AJAX endpoint — receives the full form schema as JSON and persists it."""
template = InspectionTemplate.query.get_or_404(template_id)
data = request.get_json(silent=True)
if data is None:
return jsonify({'success': False, 'error': 'Invalid JSON payload'}), 400
fields = data.get('fields', [])
# Basic sanitisation — ensure each field has the minimum required keys
sanitised = []
for field in fields:
if not isinstance(field, dict):
continue
if not field.get('id') or not field.get('type'):
continue
ftype = str(field.get('type', 'text'))
entry = {
'id': str(field.get('id', '')),
'type': ftype,
'label': str(field.get('label', 'Untitled'))[:255],
'placeholder': str(field.get('placeholder', ''))[:255],
'required': bool(field.get('required', False)),
'options': field.get('options', []) if ftype in ('radio', 'checkbox_group', 'select') else [],
'help_text': str(field.get('help_text', ''))[:500],
'order': int(field.get('order', 0)),
# Grid position & size
'col': max(1, min(12, int(field.get('col', 1)))),
'row': max(1, min(9999, int(field.get('row', 1)))),
'colSpan': max(1, min(12, int(field.get('colSpan', 6)))),
'rowSpan': max(1, min(20, int(field.get('rowSpan', 2)))),
}
# Table-specific fields
if ftype == 'table':
raw_hdrs = field.get('col_headers', ['Column 1', 'Column 2', 'Column 3'])
col_headers = [str(h)[:100] for h in raw_hdrs if isinstance(h, str)][:20] or ['Column 1']
entry['col_headers'] = col_headers
entry['table_cols'] = len(col_headers)
entry['table_rows'] = max(1, min(30, int(field.get('table_rows', 3))))
# Label-specific fields
if ftype == 'label':
entry['text_content'] = str(field.get('text_content', 'Label text'))[:2000]
entry['font_size'] = field.get('font_size', 'normal') if field.get('font_size') in ('small','normal','large','x-large') else 'normal'
entry['font_weight'] = 'bold' if field.get('font_weight') == 'bold' else 'normal'
# Button-specific fields
if ftype in ('button_submit', 'button_print', 'button_email'):
defaults = {'button_submit':'Submit Form','button_print':'Print Form','button_email':'Email Form'}
entry['btn_label'] = str(field.get('btn_label', defaults[ftype]))[:100]
sanitised.append(entry)
template.form_schema = sanitised
db.session.commit()
return jsonify({'success': True, 'field_count': len(sanitised)})
@bp.route('/<int:template_id>/form-editor/preview')
@login_required
def form_preview(template_id):
"""Renders a read-only preview of the dynamic form."""
template = InspectionTemplate.query.get_or_404(template_id)
form_fields = template.get_form_schema()
return render_template(
'templates/form_preview.html',
template=template,
form_fields=form_fields
)
# ---------------------------------------------------------------------------
# Checklist Item Management (legacy, kept for backwards compatibility)
# ---------------------------------------------------------------------------
@bp.route('/<int:template_id>/items/new', methods=['GET', 'POST'])
@login_required
@@ -97,12 +266,11 @@ def delete_template(template_id):
def create_checklist_item(template_id):
template = InspectionTemplate.query.get_or_404(template_id)
form = ChecklistItemForm()
if form.validate_on_submit():
# Get the highest display order
max_order = db.session.query(db.func.max(ChecklistItem.display_order))\
.filter_by(template_id=template.id).scalar() or 0
item = ChecklistItem(
template_id=template.id,
category=form.category.data,
@@ -112,14 +280,19 @@ def create_checklist_item(template_id):
requires_photo=form.requires_photo.data,
display_order=max_order + 1
)
db.session.add(item)
db.session.commit()
flash('Checklist item added successfully.', 'success')
return redirect(url_for('templates.edit_template', template_id=template.id))
return render_template('templates/item_form.html', form=form, template=template, title='Add Checklist Item')
return render_template(
'templates/item_form.html',
form=form,
template=template,
title='Add Checklist Item'
)
@bp.route('/items/<int:item_id>/edit', methods=['GET', 'POST'])
@login_required
@@ -127,19 +300,25 @@ def create_checklist_item(template_id):
def edit_checklist_item(item_id):
item = ChecklistItem.query.get_or_404(item_id)
form = ChecklistItemForm(obj=item)
if form.validate_on_submit():
item.category = form.category.data
item.item_description = form.item_description.data
item.scoring_type = form.scoring_type.data
item.weight = form.weight.data
item.requires_photo = form.requires_photo.data
db.session.commit()
flash('Checklist item updated successfully.', 'success')
return redirect(url_for('templates.edit_template', template_id=item.template_id))
return render_template('templates/item_form.html', form=form, item=item, template=item.template, title='Edit Checklist Item')
return render_template(
'templates/item_form.html',
form=form,
item=item,
template=item.template,
title='Edit Checklist Item'
)
@bp.route('/items/<int:item_id>/delete', methods=['POST'])
@login_required
@@ -147,24 +326,23 @@ def edit_checklist_item(item_id):
def delete_checklist_item(item_id):
item = ChecklistItem.query.get_or_404(item_id)
template_id = item.template_id
db.session.delete(item)
db.session.commit()
flash('Checklist item deleted successfully.', 'success')
return redirect(url_for('templates.edit_template', template_id=template_id))
@bp.route('/<int:template_id>/items/reorder', methods=['POST'])
@login_required
@supervisor_required
def reorder_items(template_id):
template = InspectionTemplate.query.get_or_404(template_id)
item_order = request.json.get('item_order', [])
for index, item_id in enumerate(item_order):
item = ChecklistItem.query.get(item_id)
if item and item.template_id == template.id:
item.display_order = index
db.session.commit()
return jsonify({'success': True})