Sep 18 - Update dashboard, PMs will see the QR codes of their assigned projects/buidlings only

This commit is contained in:
2026-09-18 12:48:38 -04:00
parent 13b56fb1d1
commit 3dd98a7cac
2 changed files with 24 additions and 5 deletions
+14
View File
@@ -218,6 +218,12 @@ no assignments, or a permission lookup error, sees nothing). Applied in:
`time_attendance_locations_api`, `search_employees_api`, `get_project_locations_api` `time_attendance_locations_api`, `search_employees_api`, `get_project_locations_api`
- `routes/dashboard.py`: dashboard QR list + project list, `project_qr_codes` (403-style redirect - `routes/dashboard.py`: dashboard QR list + project list, `project_qr_codes` (403-style redirect
for someone else's project), `dashboard_stats_api`, `dashboard_realtime_api` for someone else's project), `dashboard_stats_api`, `dashboard_realtime_api`
- **Projects and locations combine with AND, never OR** (Set 28) — assigned projects, narrowed to
the assigned locations when there are any. The report has always worked this way
(`_attendance_filter_conditions`); the dashboard used OR, so a PM assigned location "Bldg A"
also saw every OTHER project's QR codes that used that same location name.
A PM with locations but **no** project assignment still sees those locations across projects —
that is the only way such a user sees anything at all
- A PM scoped only by **locations** is resolved to the projects behind those locations when - A PM scoped only by **locations** is resolved to the projects behind those locations when
employee names are searched, so their filter still works without exposing other projects. employee names are searched, so their filter still works without exposing other projects.
- **Any new endpoint returning attendance, employee, QR or project rows must call it.** - **Any new endpoint returning attendance, employee, QR or project rows must call it.**
@@ -1417,6 +1423,14 @@ it (the workers share no pub/sub).
| `app.py` | `CURRENT_YEAR` from local time | | `app.py` | `CURRENT_YEAR` from local time |
| — | Verified offline (42 checks): 16 date cases + 6 rejections, partial-batch rollback against a fake session, PM scope loading, scoped vs unscoped SQL for both locations APIs (values bound as parameters), empty result for an unassigned PM, 9 manual-ID cases, and no `utcnow()` left in the two route files. Suites still green: 71 (step 1), 24 (overtime), 38 (work-type IDs). Not run against MySQL or a browser | | — | Verified offline (42 checks): 16 date cases + 6 rejections, partial-batch rollback against a fake session, PM scope loading, scoped vs unscoped SQL for both locations APIs (values bound as parameters), empty result for an unassigned PM, 9 manual-ID cases, and no `utcnow()` left in the two route files. Suites still green: 71 (step 1), 24 (overtime), 38 (work-type IDs). Not run against MySQL or a browser |
### Set 28 — Dashboard Showed a PM Other Projects' QR Codes (Sept 18, 2026)
| File | Fix |
|---|---|
| `routes/dashboard.py` | `_project_manager_qr_filter()` combined the assigned projects and locations with `or_`, so a PM assigned a location saw every project's QR codes carrying that location name. Now `and_`, matching the Attendance Report (§4). The same OR appeared in `dashboard_stats_api` and `dashboard_realtime_api` (counts and recent activity) — both switched too; `or_` is no longer imported |
| — | Verified against the real models on in-memory SQLite, committed vs edited module: PM with project Alpha + location "Bldg A" went from `A1, A2, B1` (B1 belongs to project Beta) to `A1`; project-only PM unchanged (`A1, A2`); location-only PM unchanged (`A1, B1`); PM with no assignments sees nothing; admin unfiltered. The Set 23/27 suites could not be re-run (scratch scripts from that session are gone), but the change is confined to `routes/dashboard.py`, which they do not cover |
| — | Deploy: `routes/dashboard.py` only, then restart. No migration |
--- ---
## 21. Infrastructure & Deployment ## 21. Infrastructure & Deployment
+10 -5
View File
@@ -8,7 +8,7 @@ Routes: /dashboard, /project/<id>/qr-codes, /dashboard/search,
""" """
from flask import abort, Blueprint, render_template, request, redirect, flash, session, jsonify, url_for from flask import abort, Blueprint, render_template, request, redirect, flash, session, jsonify, url_for
from datetime import datetime, timedelta, date, time from datetime import datetime, timedelta, date, time
from sqlalchemy import or_ from sqlalchemy import and_
from extensions import db, logger_handler from extensions import db, logger_handler
from models.attendance import AttendanceData from models.attendance import AttendanceData
@@ -29,6 +29,11 @@ def _project_manager_qr_filter():
if not is_pm: if not is_pm:
return False, None return False, None
# AND, not OR — the same rule the Attendance Report applies
# (_attendance_filter_conditions in routes/attendance.py): the assigned
# projects, narrowed to the assigned locations when there are any.
# With OR, a PM assigned location "Bldg A" also saw every OTHER project's
# QR codes that happened to use that same location name (Set 28).
scope = [] scope = []
if allowed_project_ids: if allowed_project_ids:
scope.append(QRCode.project_id.in_(allowed_project_ids)) scope.append(QRCode.project_id.in_(allowed_project_ids))
@@ -36,7 +41,7 @@ def _project_manager_qr_filter():
scope.append(QRCode.location.in_(allowed_location_names)) scope.append(QRCode.location.in_(allowed_location_names))
if not scope: if not scope:
return True, QRCode.id.is_(None) # assigned nothing → sees nothing return True, QRCode.id.is_(None) # assigned nothing → sees nothing
return True, or_(*scope) return True, and_(*scope)
bp = Blueprint('dashboard', __name__) bp = Blueprint('dashboard', __name__)
@@ -220,7 +225,7 @@ def dashboard_stats_api():
project_base_query = project_base_query.filter(Project.id.is_(None)) project_base_query = project_base_query.filter(Project.id.is_(None))
if allowed_location_names: if allowed_location_names:
attendance_scope.append(AttendanceData.location_name.in_(allowed_location_names)) attendance_scope.append(AttendanceData.location_name.in_(allowed_location_names))
attendance_condition = or_(*attendance_scope) if attendance_scope else AttendanceData.id.is_(None) attendance_condition = and_(*attendance_scope) if attendance_scope else AttendanceData.id.is_(None)
attendance_base_query = attendance_base_query.filter(attendance_condition) attendance_base_query = attendance_base_query.filter(attendance_condition)
location_base_query = location_base_query.filter(attendance_condition) location_base_query = location_base_query.filter(attendance_condition)
@@ -298,8 +303,8 @@ def dashboard_realtime_api():
)) ))
if allowed_location_names: if allowed_location_names:
recent_scope.append(AttendanceData.location_name.in_(allowed_location_names)) recent_scope.append(AttendanceData.location_name.in_(allowed_location_names))
recent_query = recent_query.filter(or_(*recent_scope) if recent_scope recent_query = recent_query.filter(and_(*recent_scope) if recent_scope
else AttendanceData.id.is_(None)) else AttendanceData.id.is_(None))
recent_activity = recent_query.order_by( recent_activity = recent_query.order_by(
AttendanceData.check_in_date.desc(), AttendanceData.check_in_date.desc(),