Server settings → People → Plugins: a plugin switched off for everybody shows as on and theirs to change #490

Closed
opened 2026-10-01 10:47:04 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the per-person plugin page against policy.decide and rendering it in a test.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at core/server_views.py:1059-1063 (_policy_rows), core/server_views.py:1198 (context["defaults"]), core/server_views.py:1136-1139 (_save_policies) and plugins/policy.py:184.

The per-person plugin page draws each row from that person's own policy row only, and treats a missing row as available. What actually applies to the person falls back to the instance row when they have none. So a plugin an administrator has switched off, or forced on, for everybody is drawn on a person's page as on and "This person may change it", which is not what that person gets; and there is no way to store "available" for one person as an exception to an instance-wide decision.

  • Where: core/server_views.py:1060-1062 (_policy_rows: PluginPolicy.objects.filter(person=person), and state = row.state if row else PluginPolicy.State.AVAILABLE); core/server_views.py:1197 (context["defaults"], computed and never read by templates/server/person_plugins.html); plugins/policy.py:193 (row = rows.get(getattr(person, "pk", None)) or rows.get(None)); core/server_views.py:1134-1138 (_save_policies deletes a person's row when the wanted state is available).
  • What happens: with an instance-wide forced off row for schema.org, policy.decide("schema.org", person) returns on=False, theirs=False, decided_by="administrator", and the person's page renders name="on:schema.org" checked with name="free:schema.org" checked. Saving that page with both boxes ticked stores nothing (there is no person row to delete), and the instance row still applies, so an administrator trying to give one member the plugin back leaves the page with "Nothing was different." and the member still without it.
  • Why it matters: this is the page an administrator uses to see and change what one person gets, and it shows the opposite of what applies. The person is told the truth on their own Settings page, so the administrator and the member see different answers.
  • Fix: build the person's rows from the effective state: their own row if any, else the instance row, else available, and mark rows that follow the instance default (the defaults the view already computes). In _save_policies for a person, store an explicit available row when the instance row is not available, and delete the person's row only when what was submitted equals the instance default.

Done when

  • tests/test_plugin_policy.py has a test that, with an instance-wide forced off row, the person's page renders the switch unticked and says it follows the instance default; and one that saving on and free for that person makes policy.decide return on=True, theirs=True. Both fail today.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the per-person plugin page against `policy.decide` and rendering it in a test. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `core/server_views.py:1059-1063` (`_policy_rows`), `core/server_views.py:1198` (`context["defaults"]`), `core/server_views.py:1136-1139` (`_save_policies`) and `plugins/policy.py:184`. The per-person plugin page draws each row from that person's own policy row only, and treats a missing row as *available*. What actually applies to the person falls back to the instance row when they have none. So a plugin an administrator has switched off, or forced on, for everybody is drawn on a person's page as on and "This person may change it", which is not what that person gets; and there is no way to store "available" for one person as an exception to an instance-wide decision. - **Where:** `core/server_views.py:1060-1062` (`_policy_rows`: `PluginPolicy.objects.filter(person=person)`, and `state = row.state if row else PluginPolicy.State.AVAILABLE`); `core/server_views.py:1197` (`context["defaults"]`, computed and never read by `templates/server/person_plugins.html`); `plugins/policy.py:193` (`row = rows.get(getattr(person, "pk", None)) or rows.get(None)`); `core/server_views.py:1134-1138` (`_save_policies` deletes a person's row when the wanted state is *available*). - **What happens:** with an instance-wide *forced off* row for `schema.org`, `policy.decide("schema.org", person)` returns `on=False, theirs=False, decided_by="administrator"`, and the person's page renders `name="on:schema.org" checked` with `name="free:schema.org" checked`. Saving that page with both boxes ticked stores nothing (there is no person row to delete), and the instance row still applies, so an administrator trying to give one member the plugin back leaves the page with "Nothing was different." and the member still without it. - **Why it matters:** this is the page an administrator uses to see and change what one person gets, and it shows the opposite of what applies. The person is told the truth on their own Settings page, so the administrator and the member see different answers. - **Fix:** build the person's rows from the effective state: their own row if any, else the instance row, else *available*, and mark rows that follow the instance default (the `defaults` the view already computes). In `_save_policies` for a person, store an explicit *available* row when the instance row is not *available*, and delete the person's row only when what was submitted equals the instance default. ## Done when - `tests/test_plugin_policy.py` has a test that, with an instance-wide *forced off* row, the person's page renders the switch unticked and says it follows the instance default; and one that saving *on* and *free* for that person makes `policy.decide` return `on=True, theirs=True`. Both fail today.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:47:04 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#490
No description provided.