Jul 8 - Implement Regenerate token and buik print QR codes

This commit is contained in:
2026-07-08 13:27:49 -04:00
parent ff3c76c0d8
commit 39feaa4704
5 changed files with 145 additions and 4 deletions
+1 -1
View File
@@ -400,7 +400,7 @@ contract_notification_recipients:
|---|---|---|
| `auth` | `/auth` | `/login`, `/logout`, `/profile`, `/users/*`, `/notification-matrix` |
| `dashboard` | `/` | `GET /`, `/facility-trend` (AJAX) |
| `facilities` | `/facilities` | CRUD + area management + QR code (`/<id>/qr` printable page, `/<id>/qr.png` image staff only, customers 403) |
| `facilities` | `/facilities` | CRUD + area management + QR code: `/<id>/qr` printable page, `/<id>/qr.png` image, `POST /<id>/qr/regenerate` (admin/director — invalidates old printed code), `/qr/print-all[?contract_id=]` bulk sheet. All QR routes are staff-only (customers 403); inspectors scoped to contracted facilities. |
| `public` | `/f` | **No login.** `GET /<token>` occupant facility summary; `POST /<token>/report` occupant issue report (rate-limited `5/hour`, honeypot). Resolves ACTIVE facility by `public_token` or 404. |
| `projects` | `/projects` | CRUD + customer assignment management + notification-recipient add/remove (`/<id>/notify-recipients/add`, `/notify-recipients/<rid>/remove` — admin only) |
| `customers` | `/customers` | list, invite, set-password, manage, import CSV |
+60
View File
@@ -151,6 +151,66 @@ def facility_qr_page(facility_id):
return render_template('facilities/qr.html',
facility=facility, public_url=public_url)
@bp.route('/<int:facility_id>/qr/regenerate', methods=['POST'])
@login_required
@supervisor_required
def facility_qr_regenerate(facility_id):
"""Mint a NEW public token, invalidating any previously printed QR code."""
facility = db.session.get(Facility, facility_id)
if facility is None:
abort(404)
facility.public_token = Facility.generate_public_token()
db.session.commit()
logger.info('FACILITIES | qr_regenerate | user=%s | facility_id=%s',
current_user.username, facility.id)
log_action(ACTION_UPDATE, 'Facility', facility.id, facility.name,
'regenerated public QR token (old code invalidated)')
flash('QR code regenerated. Any previously printed codes for this facility no '
'longer work — reprint and repost.', 'warning')
return redirect(url_for('facilities.facility_qr_page', facility_id=facility.id))
@bp.route('/qr/print-all')
@login_required
def facility_qr_print_all():
"""Printable sheet of QR codes for all facilities the user can see.
Optional ?contract_id=<id> limits the sheet to one contract. Customers have
no QR access (403); inspectors are scoped to their contracted facilities.
"""
if current_user.role == 'customer':
abort(403)
contract_id = request.args.get('contract_id', type=int)
if current_user.role == 'inspector':
fids = get_inspector_scope(current_user) or []
query = Facility.query.filter(Facility.id.in_(fids), Facility.active == True)
else:
query = Facility.query.filter(Facility.active == True)
if contract_id:
query = query.filter(Facility.project_id == contract_id)
facilities = query.order_by(Facility.name).all()
# Ensure every facility on the sheet has a token so its qr.png renders.
changed = False
for f in facilities:
if not f.public_token:
f.ensure_public_token()
changed = True
if changed:
db.session.commit()
selected_contract = db.session.get(Project, contract_id) if contract_id else None
return render_template('facilities/qr_print_all.html',
facilities=facilities,
selected_contract=selected_contract)
@bp.route('/<int:facility_id>/edit', methods=['GET', 'POST'])
@login_required
@supervisor_required
+6
View File
@@ -8,6 +8,12 @@
<h2><i class="bi bi-building"></i> Facilities</h2>
</div>
<div class="col-md-6 text-end">
{% if current_user.role != 'customer' %}
<a href="{{ url_for('facilities.facility_qr_print_all') }}"
class="btn btn-outline-dark" title="Printable sheet of every facility's QR code">
<i class="bi bi-qr-code"></i> Print All QR Codes
</a>
{% endif %}
{% if current_user.role in ['admin', 'director'] %}
<a href="{{ url_for('facilities.create_facility') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add Facility
+13
View File
@@ -16,10 +16,23 @@
class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> Back to Facility
</a>
<div class="d-flex gap-2">
{% if current_user.role in ['admin', 'director'] %}
<form method="POST"
action="{{ url_for('facilities.facility_qr_regenerate', facility_id=facility.id) }}"
onsubmit="return confirm('Regenerate this QR code? Any codes already printed and posted for {{ facility.name }} will STOP working and must be reprinted.');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-outline-danger btn-sm"
title="Invalidate the current code and generate a new one">
<i class="bi bi-arrow-repeat"></i> Regenerate
</button>
</form>
{% endif %}
<button onclick="window.print()" class="btn btn-primary btn-sm">
<i class="bi bi-printer"></i> Print
</button>
</div>
</div>
<div class="card shadow-sm qr-card">
<div class="card-body text-center p-4">
@@ -0,0 +1,62 @@
{% extends "base.html" %}
{% block title %}Print QR Codes{% endblock %}
{% block content %}
<style>
.qr-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.qr-item { break-inside: avoid; page-break-inside: avoid; text-align: center; }
.qr-item img { width: 220px; height: 220px; max-width: 100%; }
@media print {
.no-print { display: none !important; }
.navbar, nav, footer { display: none !important; }
.qr-item { border: 1px dashed #bbb !important; }
/* Two per row, comfortable for cutting/posting */
.qr-grid { gap: 8px; }
}
</style>
<div class="d-flex justify-content-between align-items-center mb-3 no-print">
<div>
<h2 class="h4 mb-0"><i class="bi bi-qr-code"></i> Facility QR Codes</h2>
<div class="text-muted small">
{% if selected_contract %}Contract: {{ selected_contract.name }} — {% endif %}
{{ facilities|length }} facilit{{ 'y' if facilities|length == 1 else 'ies' }}
</div>
</div>
<div class="d-flex gap-2">
<a href="{{ url_for('facilities.list_facilities') }}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> Back
</a>
<button onclick="window.print()" class="btn btn-primary btn-sm">
<i class="bi bi-printer"></i> Print All
</button>
</div>
</div>
{% if facilities %}
<div class="qr-grid">
{% for f in facilities %}
<div class="qr-item card shadow-sm p-3">
<div class="text-muted text-uppercase" style="font-size:.7rem;letter-spacing:.08em;">
Scan for Facility Status
</div>
<div class="fw-bold">{{ f.name }}</div>
{% if f.project %}
<div class="text-muted small mb-1">{{ f.project.name }}</div>
{% endif %}
<div>
<img src="{{ url_for('facilities.facility_qr_png', facility_id=f.id) }}"
alt="QR code for {{ f.name }}" loading="lazy">
</div>
<div class="small">Report a problem &amp; view recent quality</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">No active facilities to print QR codes for.</div>
{% endif %}
<p class="text-muted small mt-3 no-print">
Tip: print, cut along the dashed lines, and post each code at its facility.
</p>
{% endblock %}