Show the kiosk label prefix, and let a plugin declare the settings it owns
Three defects, all found on printedparts_label_prefix, all one root cause: nothing in the framework knew that setting existed. The parts kiosk runs logged out. An unauthenticated read of a setting is limited to an allowlist, the key was not on it, so the kiosk got a 404 and fell back to no prefix. An admin previewing the same page while logged in saw the prefix, which is why it looked like it worked. The same setting also looked like it would not save. The row did not exist on a site that installed the plugin before the setting was added, so the first save created it - under the placeholder category the settings API uses for keys it does not recognise, where the plugin's settings page, which lists by category, could no longer see it. The value was in the database the whole time. And the row was missing in the first place because seeding ran from on_install / on_enable, which fire only on a state transition. Neither runs again on an upgrade, so a setting added in a later plugin version never reached a site that installed an earlier one. The comment claiming enable ran every upgrade cycle was simply wrong. A plugin now declares the settings it owns in get_settings_defaults(): key, default, type, category, description, and whether a logged-out page may read it. The framework seeds declared keys at install, at enable, and on every flask plugin upgrade-all; files a first-time write under the declared category; re-homes any row left in the placeholder category, value untouched; and answers an anonymous read for keys marked public. Core carries no list of any plugin's keys. Contract 0.16.0 (additive optional hook). printedparts and printers move to the hook and floor their core_version at 0.16.0. The dev database had two rows in the misfiled state (printedparts_alert_email, employee_db_host); the first repairs itself on the next upgrade pass.
This commit is contained in:
@@ -52,6 +52,8 @@ SECRET_MASK = '********'
|
||||
# frontend/src/utils/siteSettings.js + mapConfig.js + setupState.js read before
|
||||
# login. Whole categories that are purely presentation are allowed wholesale;
|
||||
# the rest are named keys so a new integration key does not leak by default.
|
||||
# A plugin adds its own public keys by declaring public=True in
|
||||
# get_settings_defaults - core does not carry a list of every plugin's keys.
|
||||
PUBLIC_SETTING_CATEGORIES = {'branding', 'map'}
|
||||
PUBLIC_SETTING_KEYS = {
|
||||
'site_base_url', 'facility_name', 'printer_hostname_template',
|
||||
@@ -60,9 +62,25 @@ PUBLIC_SETTING_KEYS = {
|
||||
}
|
||||
|
||||
|
||||
def _plugin_declared_settings() -> dict:
|
||||
"""Declared settings ({key: entry}) of every loaded plugin, or {}."""
|
||||
pm = current_app.extensions.get('plugin_manager')
|
||||
if not pm:
|
||||
return {}
|
||||
try:
|
||||
return pm.get_declared_plugin_settings()
|
||||
except Exception:
|
||||
current_app.logger.exception('Could not read plugin setting declarations')
|
||||
return {}
|
||||
|
||||
|
||||
def _is_public_setting(setting) -> bool:
|
||||
return (setting.category in PUBLIC_SETTING_CATEGORIES
|
||||
or setting.key in PUBLIC_SETTING_KEYS)
|
||||
# A plugin declares its own public keys (get_settings_defaults, public=True)
|
||||
# so a kiosk or print page that renders before login can read them without
|
||||
# core carrying a list of every plugin's keys.
|
||||
if setting.category in PUBLIC_SETTING_CATEGORIES or setting.key in PUBLIC_SETTING_KEYS:
|
||||
return True
|
||||
return bool(_plugin_declared_settings().get(setting.key, {}).get('public'))
|
||||
|
||||
# Optional asset identifiers and the asset types they can be toggled on.
|
||||
# Drives per-type seed keys and the Settings matrix UI. The asset type names
|
||||
@@ -92,6 +110,17 @@ SEARCH_DOMAINS = {
|
||||
'subnet': 'Subnets',
|
||||
}
|
||||
|
||||
def _declared_default(key: str) -> dict:
|
||||
"""Return the declared default for a key (core defaults, then plugins).
|
||||
|
||||
Empty dict when nobody declares it - a genuinely ad-hoc key.
|
||||
"""
|
||||
for entry in build_default_settings():
|
||||
if entry['key'] == key:
|
||||
return entry
|
||||
return _plugin_declared_settings().get(key, {})
|
||||
|
||||
|
||||
def _is_secret(key: str) -> bool:
|
||||
return 'password' in key or 'token' in key or 'secret' in key
|
||||
|
||||
@@ -256,9 +285,19 @@ def update_setting(key: str):
|
||||
setting = Setting.query.filter_by(key=key).first()
|
||||
|
||||
# Upsert: create the row on first write (e.g. plugin config keys the setup
|
||||
# wizard saves). New keys default to a plugin-scoped string setting.
|
||||
# wizard saves). Take the category, type and description from whoever
|
||||
# declares the key - core defaults or a plugin's get_settings_defaults.
|
||||
# Filing a declared key under a placeholder category hid it from the
|
||||
# settings page that had just written it, which read as "did not save".
|
||||
if not setting:
|
||||
setting = Setting(key=key, value='', valuetype='string', category='plugin')
|
||||
declared = _declared_default(key)
|
||||
setting = Setting(
|
||||
key=key,
|
||||
value='',
|
||||
valuetype=declared.get('valuetype', 'string'),
|
||||
category=declared.get('category', 'plugin'),
|
||||
description=declared.get('description'),
|
||||
)
|
||||
db.session.add(setting)
|
||||
|
||||
# Track old value for audit
|
||||
|
||||
Reference in New Issue
Block a user