Jul 16 - Update facilities QR codes - add filters & export PDF
This commit is contained in:
+122
-15
@@ -6,7 +6,7 @@ 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.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE
|
||||
from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE, ACTION_EXPORT
|
||||
from app.utils.scope import get_customer_scope, get_inspector_scope
|
||||
|
||||
bp = Blueprint('facilities', __name__, url_prefix='/facilities')
|
||||
@@ -186,45 +186,152 @@ def facility_qr_regenerate(facility_id):
|
||||
return redirect(url_for('facilities.facility_qr_page', facility_id=facility.id))
|
||||
|
||||
|
||||
def _qr_png_bytes(url):
|
||||
"""Return PNG bytes for a QR code encoding *url* (same params as qr.png)."""
|
||||
import io
|
||||
import qrcode
|
||||
img = qrcode.make(url, box_size=10, border=2)
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format='PNG')
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
@bp.route('/qr/print-all')
|
||||
@login_required
|
||||
def facility_qr_print_all():
|
||||
"""Printable sheet of QR codes for all facilities the user can see.
|
||||
"""Printable / selectable sheet of QR codes the user can see.
|
||||
|
||||
Optional ?contract_id=<id> limits the sheet to one contract. Inspectors have
|
||||
no QR management (403); customers are scoped to their assigned facilities;
|
||||
managers see all active facilities.
|
||||
Query params (all optional):
|
||||
?contract_id=<id> — limit to one contract; narrows the facility dropdown
|
||||
?facility_id=<id> — limit to a single facility
|
||||
?include_areas=1 — also render each facility's per-area QR codes
|
||||
|
||||
Inspectors have no QR management (403); customers are scoped to their
|
||||
assigned facilities; managers see all active facilities.
|
||||
"""
|
||||
# QR management is not an inspector task.
|
||||
if current_user.role == 'inspector':
|
||||
abort(403)
|
||||
|
||||
contract_id = request.args.get('contract_id', type=int)
|
||||
contract_id = request.args.get('contract_id', type=int)
|
||||
facility_id = request.args.get('facility_id', type=int)
|
||||
include_areas = request.args.get('include_areas') in ('1', 'true', 'on')
|
||||
|
||||
# Facilities in the viewer's scope.
|
||||
if current_user.role == 'customer':
|
||||
fids = get_customer_scope(current_user) or []
|
||||
query = Facility.query.filter(Facility.id.in_(fids), Facility.active == True)
|
||||
scoped = Facility.query.filter(Facility.id.in_(fids), Facility.active == True)
|
||||
else:
|
||||
query = Facility.query.filter(Facility.active == True)
|
||||
scoped = Facility.query.filter(Facility.active == True)
|
||||
scoped_facilities = scoped.order_by(Facility.name).all()
|
||||
|
||||
if contract_id:
|
||||
query = query.filter(Facility.project_id == contract_id)
|
||||
# Contract dropdown — only contracts present among the scoped facilities.
|
||||
contract_ids = {f.project_id for f in scoped_facilities if f.project_id}
|
||||
contracts = (Project.query
|
||||
.filter(Project.id.in_(contract_ids))
|
||||
.order_by(Project.name).all()) if contract_ids else []
|
||||
|
||||
facilities = query.order_by(Facility.name).all()
|
||||
# Facility dropdown — narrowed by the selected contract.
|
||||
facility_options = [f for f in scoped_facilities
|
||||
if not contract_id or f.project_id == contract_id]
|
||||
|
||||
# Ensure every facility on the sheet has a token so its qr.png renders.
|
||||
# The rendered grid — apply the contract + facility filters.
|
||||
grid_facilities = facility_options
|
||||
if facility_id:
|
||||
grid_facilities = [f for f in grid_facilities if f.id == facility_id]
|
||||
|
||||
# Ensure every rendered facility (and area, if requested) has a token so
|
||||
# its qr.png renders; collect areas keyed by facility id.
|
||||
changed = False
|
||||
for f in facilities:
|
||||
areas_by_facility = {}
|
||||
for f in grid_facilities:
|
||||
if not f.public_token:
|
||||
f.ensure_public_token()
|
||||
changed = True
|
||||
if include_areas:
|
||||
fa = f.areas.order_by(Area.name).all()
|
||||
for a in fa:
|
||||
if not a.public_token:
|
||||
a.ensure_public_token()
|
||||
changed = True
|
||||
areas_by_facility[f.id] = fa
|
||||
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)
|
||||
facilities=grid_facilities,
|
||||
areas_by_facility=areas_by_facility,
|
||||
include_areas=include_areas,
|
||||
contracts=contracts,
|
||||
facility_options=facility_options,
|
||||
selected_contract=selected_contract,
|
||||
selected_contract_id=contract_id,
|
||||
selected_facility_id=facility_id)
|
||||
|
||||
|
||||
@bp.route('/qr/export-pdf', methods=['POST'])
|
||||
@login_required
|
||||
def facility_qr_export_pdf():
|
||||
"""Export the selected facility + area QR codes to a single PDF.
|
||||
|
||||
Selection arrives as repeated `facility_ids` / `area_ids` form fields.
|
||||
Scope is enforced per-id via the same helpers as the QR pages, so a
|
||||
customer can never export a code outside their assigned facilities.
|
||||
"""
|
||||
if current_user.role == 'inspector':
|
||||
abort(403)
|
||||
|
||||
facility_ids = request.form.getlist('facility_ids', type=int)
|
||||
area_ids = request.form.getlist('area_ids', type=int)
|
||||
|
||||
if not facility_ids and not area_ids:
|
||||
flash('Select at least one QR code to export.', 'warning')
|
||||
return redirect(request.referrer or url_for('facilities.facility_qr_print_all'))
|
||||
|
||||
items = []
|
||||
for fid in facility_ids:
|
||||
facility = _facility_for_qr_or_403(fid) # 403 if out of scope
|
||||
url = _public_facility_url(facility)
|
||||
items.append({
|
||||
'title': facility.name,
|
||||
'subtitle': facility.project.name if facility.project else None,
|
||||
'caption': 'Facility · Report a problem & view recent quality',
|
||||
'png': _qr_png_bytes(url),
|
||||
'_sort': ((facility.name or '').lower(), 0, ''),
|
||||
})
|
||||
for aid in area_ids:
|
||||
area = _area_for_qr_or_403(aid) # 403 if out of scope
|
||||
url = _public_area_url(area)
|
||||
fac_name = area.facility.name if area.facility else ''
|
||||
items.append({
|
||||
'title': area.name,
|
||||
'subtitle': fac_name or None,
|
||||
'caption': 'Area · Report a problem & view recent quality',
|
||||
'png': _qr_png_bytes(url),
|
||||
'_sort': (fac_name.lower(), 1, (area.name or '').lower()),
|
||||
})
|
||||
|
||||
# Persist any tokens minted by ensure_public_token() above.
|
||||
db.session.commit()
|
||||
|
||||
# Group each facility with its own areas: facility card first, then areas.
|
||||
items.sort(key=lambda x: x['_sort'])
|
||||
|
||||
from app.utils.pdf_export import generate_qr_codes_pdf
|
||||
summary = f'{len(facility_ids)} facilit' + ('y' if len(facility_ids) == 1 else 'ies')
|
||||
summary += f', {len(area_ids)} area' + ('' if len(area_ids) == 1 else 's')
|
||||
pdf_bytes = generate_qr_codes_pdf(items, filter_summary=summary)
|
||||
|
||||
logger.info('FACILITIES | qr_export_pdf | user=%s | facilities=%s | areas=%s',
|
||||
current_user.username, len(facility_ids), len(area_ids))
|
||||
log_action(ACTION_EXPORT, 'Facility', 0, 'QR Codes',
|
||||
f'exported {len(facility_ids)} facility + {len(area_ids)} area QR codes to PDF')
|
||||
|
||||
from flask import Response
|
||||
return Response(pdf_bytes, mimetype='application/pdf', headers={
|
||||
'Content-Disposition': 'attachment; filename="qr_codes.pdf"',
|
||||
})
|
||||
|
||||
# ── Public Area QR code ───────────────────────────────────────────────────────
|
||||
# Mirrors the facility QR routes above, but scoped to a single area. Customer
|
||||
|
||||
Reference in New Issue
Block a user