Jul 7 - Implement QR codes for facility
This commit is contained in:
@@ -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))
|
||||
Reference in New Issue
Block a user