Jul 9 - Update to allow customer to manage their facilities' QR codes
This commit is contained in:
@@ -407,6 +407,8 @@ Management (`/scheduled-inspections/new|edit|delete`) is `@project_manager_requi
|
||||
| Notification Matrix | ✅ only | ❌ | ❌ | ❌ | ❌ |
|
||||
| Customers | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| Facilities | ✅ | ✅ | ✅ | read | scoped |
|
||||
| Facility QR (view/print) | ✅ | ✅ | ✅ | ✅ | scoped |
|
||||
| Facility QR (regenerate) | ✅ | ✅ | ❌ | ❌ | scoped |
|
||||
| Contracts | ✅ | ✅ | ✅ | read | scoped |
|
||||
| Templates | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| Inspections (execute) | ✅ | ✅ | ✅ | ✅ | read |
|
||||
@@ -438,7 +440,7 @@ Management (`/scheduled-inspections/new|edit|delete`) is `@project_manager_requi
|
||||
|---|---|---|
|
||||
| `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, `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. |
|
||||
| `facilities` | `/facilities` | CRUD + area management + QR code: `/<id>/qr` printable page, `/<id>/qr.png` image, `POST /<id>/qr/regenerate` (invalidates old printed code), `/qr/print-all[?contract_id=]` bulk sheet. **Customers may use all QR actions (including regenerate) for their own assigned facilities**; inspectors/PM/admin/director for any. Scope enforced by `_facility_for_qr_or_403()` (customers) / `get_customer_scope` (print-all). Regenerate is limited to admin/director + scoped customer (PM/inspector excluded). |
|
||||
| `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 |
|
||||
|
||||
+36
-22
@@ -97,7 +97,9 @@ def view_facility(facility_id):
|
||||
return render_template('facilities/view.html', facility=facility, areas=areas)
|
||||
|
||||
|
||||
# ── Public QR code (staff-only generation) ────────────────────────────────────
|
||||
# ── Public QR code ────────────────────────────────────────────────────────────
|
||||
# Staff (admin/director/pm/inspector) may access QR for any facility; customers
|
||||
# may access QR only for facilities in their assigned scope.
|
||||
|
||||
def _public_facility_url(facility):
|
||||
"""Absolute URL the QR encodes — the login-free occupant summary page."""
|
||||
@@ -108,15 +110,27 @@ def _public_facility_url(facility):
|
||||
token=facility.public_token, _external=True)
|
||||
|
||||
|
||||
@bp.route('/<int:facility_id>/qr.png')
|
||||
@login_required
|
||||
def facility_qr_png(facility_id):
|
||||
"""Return the facility's QR code as a PNG image (staff only)."""
|
||||
if current_user.role == 'customer':
|
||||
abort(403)
|
||||
def _facility_for_qr_or_403(facility_id):
|
||||
"""Load a facility for a QR action, enforcing customer facility scope.
|
||||
|
||||
Customers may only touch QR codes for facilities they are assigned to; all
|
||||
other (staff) roles have unrestricted QR access.
|
||||
"""
|
||||
facility = db.session.get(Facility, facility_id)
|
||||
if facility is None:
|
||||
abort(404)
|
||||
if current_user.role == 'customer':
|
||||
cids = get_customer_scope(current_user) or []
|
||||
if facility.id not in cids:
|
||||
abort(403)
|
||||
return facility
|
||||
|
||||
|
||||
@bp.route('/<int:facility_id>/qr.png')
|
||||
@login_required
|
||||
def facility_qr_png(facility_id):
|
||||
"""Return the facility's QR code as a PNG image."""
|
||||
facility = _facility_for_qr_or_403(facility_id)
|
||||
|
||||
# Token may not exist for pre-phase34 rows viewed before any commit.
|
||||
created = not facility.public_token
|
||||
@@ -141,11 +155,7 @@ def facility_qr_png(facility_id):
|
||||
@login_required
|
||||
def facility_qr_page(facility_id):
|
||||
"""Printable page: facility name + QR + public URL + posting instructions."""
|
||||
if current_user.role == 'customer':
|
||||
abort(403)
|
||||
facility = db.session.get(Facility, facility_id)
|
||||
if facility is None:
|
||||
abort(404)
|
||||
facility = _facility_for_qr_or_403(facility_id)
|
||||
public_url = _public_facility_url(facility)
|
||||
db.session.commit() # persist token if it was just generated
|
||||
return render_template('facilities/qr.html',
|
||||
@@ -154,12 +164,15 @@ def facility_qr_page(facility_id):
|
||||
|
||||
@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)
|
||||
"""Mint a NEW public token, invalidating any previously printed QR code.
|
||||
|
||||
Allowed for admin/director, and for customers on their own assigned
|
||||
facilities. Project managers and inspectors cannot regenerate.
|
||||
"""
|
||||
facility = _facility_for_qr_or_403(facility_id)
|
||||
if current_user.role not in ('admin', 'director', 'customer'):
|
||||
abort(403)
|
||||
|
||||
facility.public_token = Facility.generate_public_token()
|
||||
db.session.commit()
|
||||
@@ -178,17 +191,18 @@ def facility_qr_regenerate(facility_id):
|
||||
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.
|
||||
Optional ?contract_id=<id> limits the sheet to one contract. Inspectors are
|
||||
scoped to their contracted facilities; customers to their assigned
|
||||
facilities; managers see all active 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)
|
||||
elif current_user.role == 'customer':
|
||||
fids = get_customer_scope(current_user) or []
|
||||
query = Facility.query.filter(Facility.id.in_(fids), Facility.active == True)
|
||||
else:
|
||||
query = Facility.query.filter(Facility.active == True)
|
||||
|
||||
|
||||
@@ -8,12 +8,10 @@
|
||||
<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">
|
||||
class="btn btn-outline-dark" title="Printable sheet of your facilities' QR codes">
|
||||
<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
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<i class="bi bi-arrow-left"></i> Back to Facility
|
||||
</a>
|
||||
<div class="d-flex gap-2">
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
{% if current_user.role in ['admin', 'director', 'customer'] %}
|
||||
<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.');">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<i class="bi bi-graph-up-arrow"></i> Scorecard
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.role in ['admin', 'director', 'project_manager'] %}
|
||||
{% if current_user.role in ['admin', 'director', 'project_manager', 'customer'] %}
|
||||
<a href="{{ url_for('facilities.facility_qr_page', facility_id=facility.id) }}"
|
||||
class="btn btn-outline-dark" title="Printable QR code for this facility">
|
||||
<i class="bi bi-qr-code"></i> QR Code
|
||||
|
||||
@@ -29,6 +29,22 @@ When you scan the code (or open the link), you'll see:
|
||||
> For privacy, the public page shows only summary information. It never displays
|
||||
> individual issue details, inspector names, or any other facility's data.
|
||||
|
||||
### Getting, printing, and regenerating the QR code (from your portal)
|
||||
|
||||
You can produce the QR code yourself for any facility you're assigned to:
|
||||
|
||||
- **One facility** — open the facility from your dashboard/facilities list and
|
||||
click **QR Code**. This opens a printable page with the code and its link;
|
||||
click **Print** to print it.
|
||||
- **All your facilities at once** — on the **Facilities** page click **Print All
|
||||
QR Codes** for a single sheet covering every facility you're assigned to.
|
||||
- **Regenerate** — on a facility's QR page, click **Regenerate** to issue a new
|
||||
code. Use this if a posted code is defaced or you want to retire an old one.
|
||||
**Any previously printed code for that facility stops working**, so reprint and
|
||||
repost after regenerating.
|
||||
|
||||
*[Screenshot: facility QR page with Print and Regenerate buttons]*
|
||||
|
||||
### Reporting a problem
|
||||
|
||||
1. On the facility status page, scroll to **Report a Problem**.
|
||||
|
||||
Reference in New Issue
Block a user