Jul 8 - Implement Regenerate token and buik print QR codes
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,9 +16,22 @@
|
||||
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">
|
||||
|
||||
@@ -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 & 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 %}
|
||||
Reference in New Issue
Block a user