Workspace shows only level-4 units
Problem¶
The workspace unit picker currently surfaces ancestors of the user's lab — "EPFL" (level 1), faculties / services centraux (level 2), and instituts (level 3) — alongside the actual labs (level 4) where CO₂ data is recorded. This dilutes the picker and lets users select scopes that have no own data (only aggregated children).
Expected behavior: /users/units returns only level-4 units — the leaves of the ACCRED hierarchy, i.e. the actual labs/units. No "EPFL", no faculty, no institute.
Originally reported in #855, tracked here as #930.
Decision applied¶
Backend filter — hardcode level == 4 in /users/units (mirrors the backoffice_reporting.py /affiliations precedent that hardcodes level IN (2, 3)). Frontend remains thin.
Rationale:
- Backend is the source of truth (memory rule).
- Precedent for hardcoding a level discriminator already exists at
backend/app/api/v1/backoffice_reporting.py:44. - Frontend has only one consumer (
frontend/src/stores/workspace.ts:224), and it does no level-aware logic — adding a query param would just push policy decisions into the client.
Files to change¶
| File | Function / lines | Change |
|---|---|---|
backend/app/services/unit_service.py | get_user_units() (line 57, query built at lines 99-124) | Add .where(col(Unit.level) == 4) to the query before the policy-filter block (~ line 113). |
backend/tests/unit/services/test_unit_service.py | new test cases | Cover: mixed-level user roles → only level-4 returned; user with only level-3 role → empty list. |
backend/tests/integration/v1/ | new file test_users_units_level_filter.py | Integration smoke that hits /api/v1/users/units with a fixture user having mixed-level roles. |
Optional / recommended NO:
backend/app/api/v1/users.py(lines 29-57): do not add alevelquery param. Decision keeps the rule on the server.backend/app/schemas/unit.py(UnitWithUserRole, lines 10-34): do not exposelevelto the frontend; nothing on the FE needs it now (filtering is server-side).
Approach¶
- Add the level filter. In
backend/app/services/unit_service.py::get_user_units(), after the.where(col(UnitUser.user_id) == user.id)clause (line 112), add:
query = query.where(col(Unit.level) == 4)
Use col() per the repo convention (mypy ColumnElement).
-
Confirm fixtures cover mixed levels.
backend/app/providers/test_fixtures.py:74-97already includes level-3 and level-4 test units. Verify the fixture user holdsUnitUserrows pointing at both levels; if not, add a level-3UnitUserrow so the regression test has something to filter out. -
Add unit tests on
UnitService.get_user_units()asserting the filter (mixed-level user → response excludes non-level-4). -
Add an integration test hitting
/api/v1/users/unitsend-to-end against the test DB, assertinglevelof every returned row is 4 (after re-fetching by id, since the response schema does not exposelevel). -
No frontend change.
frontend/src/stores/workspace.ts:224(api.get('users/units')) andLabSelectorItem.vuerender whatever the API returns. Confirmed viagrep -rn "users/units" frontend/src/— single consumer.
Tests¶
Proposed test functions (all new):
tests/unit/services/test_unit_service.pytest_get_user_units_returns_only_level_4_when_user_has_mixed_level_rolestest_get_user_units_returns_empty_when_user_only_has_non_level_4_roles-
test_get_user_units_unchanged_for_user_with_only_level_4_roles(no-regression) -
tests/integration/v1/test_users_units_level_filter.py test_users_units_endpoint_filters_to_level_4
Verification¶
cd backend
uv run pytest tests/integration/v1/test_users_units_level_filter.py -xvs
uv run pytest tests/unit/services/test_unit_service.py -xvs
make backend-dev
# log in as a user with mixed-level roles in dev DB; curl /api/v1/users/units; verify only level-4 entries
cd ../frontend
bun run lint && bun run typecheck # should be no-op (no FE change)
# Manual: bun run dev → workspace setup page → verify dropdown shows only labs (no "EPFL", no faculties)
Resolved questions¶
-
Is
level == 4the right discriminator for ALL providers? Kept aslevel == 4in delivery. ACCRED is the only provider today and returns levels 1-4 (backend/app/providers/unit_provider.py:180-218); this also matches the existingbackoffice_reporting.py:44precedent (level IN (2, 3)). When a second provider lands, revisit with leaf semantics (Unit.children == Nonevia self-join or anis_leafcolumn) or a provider-scopedleaf_levelconfig. The comment on the new.where()clause documents the assumption inline. -
Does any other endpoint or frontend code rely on
/users/unitsreturning higher-level units? - Frontend: single consumer at
frontend/src/stores/workspace.ts:224— only rendersname,affiliations,current_user_role, no level-aware branching. No regression. -
Backend:
UnitService.get_user_units()is also reached viaGET /api/v1/units(backend/app/api/v1/units.py:44). That endpoint has no frontend consumer (grep -rn "'units'" frontend/src/returns noapi.get('units')call — onlyapi.get('users/units')andapi.get('units/{id}')). Filtering both endpoints to level-4 is therefore safe and consistent with the workspace semantic the rule encodes. -
Should the response schema expose
levelfor any UI affordance? No. Filter stays server-side;UnitWithUserRoleunchanged.
Delivery notes¶
- Code change: 3 lines (1
.where()+ 2-line intent comment) atbackend/app/services/unit_service.py:113-115. - Unit tests: 3 new tests in
backend/tests/unit/services/test_unit_service.py::TestGetUserUnitsLevelFilter(mixed levels, only-non-4 → empty, only-4 baseline). - Integration test:
backend/tests/integration/v1/test_users_units_level_filter.py— hitsGET /api/v1/users/unitswith the real test DB session via FastAPI dependency override. - mypy clean; full backend suite (1478 tests) passes.