Skip to content

Plugin settings form and settings page (M5 part 3a-1 + 3a-2) - #256

Merged
txoof merged 4 commits into
mainfrom
238-settings-ui
Oct 9, 2026
Merged

txoof merged 4 commits into
mainfrom
238-settings-ui

Conversation

@txoof-bot

@txoof-bot txoof-bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #238. Both halves of M5 part 3a in one PR (no stack): 3a-1 (form logic, was #255 / #252) and 3a-2 (settings page, was #253). You reviewed both already; the content is unchanged. One squash-merge lands all of it.

Part 3a-1: form logic and saving

What changed

The logic behind the plugin settings form. The page itself comes in 3a-2; this part has no new page yet (like 2b-1 was).

  • web/forms.py
    • fields() builds the form's fields from a plugin's settings description: choice list, check box, number (with its limits), text (with its length limit), secret (never put on the page), pick-several list. A setting of a kind the form can't show (a group of settings) is shown as it is in the file, to be changed there.
    • Groups as agreed: the name, then the plugin's own settings, then display time / refresh / layout / level, then the rest ("More settings").
    • Each field shows the value in the file, or the default (including the plugin's suggestions for refresh, layout and storage).
    • read() turns a sent form into the changes to save, or an error next to each field (then nothing is saved). The checks are the plugin's own settings description and the shared one, the same as the config check.
  • config_file.set_settings() writes those changes into one [[plugin]] block as text, keeping everything else.
    • A new value takes the place of its # key = ... comment line, so the help text above it stays.
    • A value changed back to its default becomes # key = default again.
    • name goes right below id.
    • A value written over several lines is never changed.
    • As in part 2, the result is read back and compared before saving.
  • PluginEditor.settings() / save_settings() join these under the config-file lock and apply the change at once. Saving the same form twice writes nothing the second time.
  • pytest-xdist as a dev dependency: uv run pytest -n auto runs the tests on all 4 cores (about 1.5 instead of 4.5 minutes on the Pi). CLAUDE.md says when to run which tests.
  • example.toml_value, choices and plain are now public (the form uses them). Notes added to docs/decisions/web-interface.md.

Choices made without asking:

  • A field missing from the form is left as it is. The page (3a-2) sends a hidden empty value for each check box and list, because browsers send nothing for an unticked box.
  • A value left as it is stays in the file, even one written there that equals the default. Only a value the user changes to its default is taken out.
  • Numbers must be plain digits (12, -1.5). 1_0, nan, 1e3 and other scripts' digits are refused.
  • A list picks each choice at most once.

Size: +937/−24 (about 960, of which 24 is uv.lock and about half is tests). That is over the guideline; it grew with the review fixes, which added tests for the cases they found.

Area

web (src/paperpi/web/forms.py, config_file.py, plugins.py), core (example.py: three helpers made public), ci (pyproject.toml, uv.lock: pytest-xdist), docs (CLAUDE.md, docs/decisions/web-interface.md).

Tests

New: tests/test_web_forms.py (fields, groups, kinds, reading, errors, secrets, lists, numbers, shown again after errors), the settings part of tests/test_web_config_file.py (comment lines, defaults, quoted keys, similar keys, values over several lines, CRLF), and save_settings in tests/test_web_plugins.py.

  • uv run pytest passes (1416 with the 3a-2 page on top, uv run pytest -n auto on the Pi)
  • uv run ruff check . passes
  • Hardware tests run on the Pi (only if display or driver code changed): not needed

Review

  • Review agents' comments answered: 2 local reviews (code + tests, security + docs) before the first push, all findings fixed in the second commit.
    • Blocking one found: a list from the file (a list) never matched the form's value (a tuple), so every save rewrote it.
    • Also fixed: a secret typed into a form with errors was kept to be shown again; repeated list values made a huge file; loose number parsing; a new setting could be put inside a value written over several lines; check boxes and lists shown wrongly after errors.

Part 3a-2: the settings page

What changed

The plugin settings page: /plugins/<n>/settings?id=<id>.

  • Layout as agreed (2026-10-08):
    • The plugin's name comes first.
    • Then "Settings of this plugin", then "When and how it is shown" (display time, refresh, layout, level).
    • The other shared settings are folded under More settings. It opens by itself when one of them has an error or a value that is not the default.
    • A folded Technical information part has the id, type, storage folder and the place in the config file.
  • Each field: help text, (required) where needed, the setting's key, the limits for numbers, and "Default: …".
    • A secret (an API key) is a password field that never shows the saved value. Leave it empty to keep it.
    • A setting the form can't show (a group of settings) is shown as written in the file, to be changed there. If it holds a secret, it is shown only as "set".
  • Save writes only what changed and applies it at once (3a-1). It comes back with "Saved." If any value has an error, nothing is saved, and each error is shown next to its field with what was typed (a typed secret is never shown again).
  • Active Plugins has a Settings link for each plugin. The "type them into the config file" notes are gone.
  • Plugin Library: after Add, the new plugin's settings page opens: "Added. Fill in its settings here, then Save."
  • Setting helpers:
    • paperpi.plugin.setting(..., helper="location") names a helper for a setting.
    • paperpi.web.helpers.HELPERS holds a function per name that adds HTML under the field. A name it doesn't know adds nothing, so the plain field still works.
    • The list is empty now; the first helper (location) comes in 3c.
  • Docs: README web section, docs/writing-plugins.md (which field each setting type gets; helper=), docs/decisions/web-interface.md, the address table in app.py.

Choices made without asking:

  • Numbers are plain text fields, not the browser's number field. The phone number keypad is offered only for numbers that can't be below 0, because many phone keypads have no minus key (latitude in Rio is negative).
  • A choice whose value in the file is not one of the choices shows "(not one of the choices: …)" instead of quietly picking the first choice.
  • "Saved." is also shown when nothing changed.

Follow-up (not in this PR): some shared help texts were written for the config file ("see Layouts above", "leave it out to use…") and read a little oddly on the page. A later PR can give the page its own short wording.

Area

web (src/paperpi/web/: app.py, forms.py, helpers.py, plugins.py, templates, style.css), core (plugin.py: setting(helper=), helper_of), docs.

Tests

New tests/test_web_settings.py: link from Active Plugins, groups and folding, saving and applying, errors shown again with nothing saved, check boxes on and off, add → settings page, changed meanwhile, unknown type, out-of-range place, a value not in the choices, secrets never on the page, a list to tick, a read-only setting, the hand-edits note, the helper hook.

  • uv run pytest passes (1416 on the Pi, -n auto)
  • uv run ruff check . passes
  • Hardware tests run on the Pi (only if display or driver code changed): not needed

Images

No screenshot yet: there is no browser on the Pi in this session. The page's HTML is checked by the tests. I can add a screenshot from another device if you want one.

Review

  • Review agents' comments answered: 2 local reviews (code + tests, security + docs) before the first push; all findings fixed in the second commit, except two small ones:
    • The page reads the file twice per view.
    • A plugin of an unknown type still gets a Settings link, which shows the list with a clear message.

Only txoof approves and merges this PR.

🤖 Generated with Claude Code

txoof-bot and others added 4 commits October 9, 2026 06:32
…o a plugin block (M5 part 3a-1)

forms.fields() builds the settings form's fields from a plugin's settings
description (groups: name, own, common, more); forms.read() turns a sent form
into checked changes with an error per field. config_file.set_settings()
writes them into one [[plugin]] block (defaults back to comments, read-back
check). PluginEditor.save_settings() ties them together. pytest-xdist added
as a dev dependency (uv run pytest -n auto).

Part of #238

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…et echoed, values over several lines

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/plugins/<n>/settings shows every setting of a plugin, built from its
settings description, in the agreed groups (name; own settings; display
time, refresh, layout, level; the rest folded under More settings, open by
itself when needed; Technical information with the id). Save applies at
once; errors show next to their fields with what was typed. Active Plugins
links to it, and adding from the Library opens it. setting(helper=...) names
a web helper shown under the field (paperpi.web.helpers; none built in yet).

Part of #238

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…More settings rule, tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@txoof-bot
txoof-bot requested a review from txoof as a code owner October 9, 2026 04:37
@txoof
txoof added this pull request to stack #257 October 9, 2026 04:42
txoof
txoof previously approved these changes Oct 9, 2026
@txoof-bot

Copy link
Copy Markdown
Collaborator Author

Folded into #255 (its branch now holds both parts), so one PR merges 3a-1 and 3a-2 together.

@txoof-bot txoof-bot closed this Oct 9, 2026
@txoof-bot txoof-bot reopened this Oct 9, 2026
@txoof-bot
txoof-bot removed this pull request from stack #257 October 9, 2026 18:30
@txoof-bot
txoof-bot changed the base branch from 238-settings-logic to main October 9, 2026 18:30
@txoof-bot
txoof-bot dismissed txoof’s stale review October 9, 2026 18:30

The base branch was changed.

@txoof-bot txoof-bot changed the title Plugin settings page (M5 part 3a-2) Plugin settings form and settings page (M5 part 3a-1 + 3a-2) Oct 9, 2026
@txoof
txoof merged commit e0440d2 into main Oct 9, 2026
4 checks passed
@txoof
txoof deleted the 238-settings-ui branch October 9, 2026 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants