Sep 18 - Update dashboard, PMs will see the QR codes of their assigned projects/buidlings only
This commit is contained in:
@@ -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
@@ -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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user