Jul 7 - Implement QR codes for facility

This commit is contained in:
2026-07-07 21:05:07 -04:00
parent 8b5a4758e5
commit faab9fd008
12 changed files with 684 additions and 7 deletions
+79 -2
View File
@@ -5,7 +5,7 @@ from app import db
from app.models.facility import Facility, Area
from app.models.project import Project
from app.utils.forms import FacilityForm, AreaForm
from app.utils.decorators import supervisor_required, admin_required
from app.utils.decorators import supervisor_required, admin_required, project_manager_required
from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE
from app.utils.scope import get_customer_scope, get_inspector_scope
from app.tenancy.gates import quota_soft_check
@@ -237,4 +237,81 @@ def delete_area(area_id):
current_user.username, area_id_snap, area_name)
log_action(ACTION_DELETE, 'Area', area_id_snap, area_name)
flash(f'Area "{area_name}" deleted successfully.', 'success')
return redirect(url_for('facilities.view_facility', facility_id=facility_id))
return redirect(url_for('facilities.view_facility', facility_id=facility_id))
# ── Facility QR codes (phase38) ───────────────────────────────────────────────
def _qr_scan_url(facility):
"""Absolute public scan URL, built from the current host (rule 64 pattern)."""
return request.host_url.rstrip('/') + url_for('facility_qr.scan',
token=facility.qr_token)
@bp.route('/<int:facility_id>/qr')
@login_required
@project_manager_required
def qr_card(facility_id):
"""Printable QR card for one facility. Generates the token on first use."""
from app.utils.qr import qr_svg
facility = db.session.get(Facility, facility_id)
if facility is None:
abort(404)
if not facility.qr_token:
facility.ensure_qr_token()
db.session.commit()
logger.info('FACILITIES | qr_token_created | user=%s | facility_id=%s',
current_user.username, facility_id)
scan_url = _qr_scan_url(facility)
return render_template('facilities/qr_card.html',
facility=facility,
scan_url=scan_url,
svg=qr_svg(scan_url))
@bp.route('/qr-sheet')
@login_required
@project_manager_required
def qr_sheet():
"""Bulk print sheet — one labeled QR card per active facility."""
from app.utils.qr import qr_svg
facilities = Facility.query.filter_by(active=True).order_by(Facility.name).all()
generated = 0
for f in facilities:
if not f.qr_token:
f.ensure_qr_token()
generated += 1
if generated:
db.session.commit()
logger.info('FACILITIES | qr_tokens_created | user=%s | count=%s',
current_user.username, generated)
cards = [{'facility': f, 'svg': qr_svg(_qr_scan_url(f))} for f in facilities]
return render_template('facilities/qr_sheet.html', cards=cards)
@bp.route('/<int:facility_id>/qr/regenerate', methods=['POST'])
@login_required
@supervisor_required
def regenerate_qr(facility_id):
"""Rotate the QR token — invalidates every previously printed poster."""
import secrets
facility = db.session.get(Facility, facility_id)
if facility is None:
abort(404)
facility.qr_token = secrets.token_urlsafe(32)
db.session.commit()
logger.info('FACILITIES | qr_token_regenerated | user=%s | facility_id=%s',
current_user.username, facility_id)
log_action(ACTION_UPDATE, 'Facility', facility.id, facility.name,
'QR token regenerated — previously printed QR posters are now invalid')
flash('QR code regenerated. Previously printed posters no longer work — '
'print and post the new code.', 'success')
return redirect(url_for('facilities.qr_card', facility_id=facility_id))