simple-module-settings 0.0.39__tar.gz → 0.0.41__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/PKG-INFO +4 -4
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/package.json +1 -1
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/pyproject.toml +4 -4
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/endpoints/module_api.py +2 -8
- simple_module_settings-0.0.41/settings/invalidation.py +70 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/module.py +14 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/ModulesEdit.tsx +5 -1
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/FieldSource.tsx +3 -3
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/ModuleFieldRow.tsx +11 -11
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/ModuleForm.tsx +22 -10
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/TestConnectionButton.tsx +16 -4
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/reload.py +11 -7
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/store.py +4 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_reload.py +17 -1
- simple_module_settings-0.0.41/tests/test_settings_commit_invalidation.py +99 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_secret_json_roundtrip.py +7 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests-js/ModuleForm.test.tsx +1 -1
- simple_module_settings-0.0.41/tests-js/settings_ModuleFieldLayout.test.tsx +62 -0
- simple_module_settings-0.0.41/tests-js/settings_ModuleFormRevert.test.tsx +88 -0
- simple_module_settings-0.0.41/tests-js/settings_TestConnectionButton.test.tsx +111 -0
- simple_module_settings-0.0.39/tests-js/settings_TestConnectionButton.test.tsx +0 -47
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/.gitignore +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/LICENSE +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/README.md +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/__init__.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/_announce.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/_field_meta.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/_listing.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/_managed_keys.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/_module_settings.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/_module_settings_props.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/_row_masking.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/_secrets.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/_unique_write.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/browse_query.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/cli.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/constants.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/contracts/__init__.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/contracts/accessor.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/contracts/events.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/contracts/invalidation.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/contracts/registry.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/contracts/schemas.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/deps.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/endpoints/__init__.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/endpoints/_create_form.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/endpoints/api.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/endpoints/tenant_api.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/endpoints/views.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/env_vars.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/errors.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/exceptions.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/hydrate.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/known_keys.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/locales/de.json +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/locales/en.json +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/models.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/module_registry.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/Browse.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/Create.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/Edit.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/api.generated.ts +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/api.ts +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/FieldInput.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/JsonInput.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/KeyField.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/ResolvedValue.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/ScopeTabs.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/StoreCards.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/StoreTable.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/ValueInput.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/last-test.ts +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/validation-errors.test.ts +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/components/validation-errors.ts +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/routes.ts +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/types.ts +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/py.typed +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/registration.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/service.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/services.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/settings.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/tenant_scope.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/tenant_view.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_clear_route_per_scope.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_cli_import.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_concurrent_overrides.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_duplicate_and_race.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_env_pinned.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_hydrate.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_hydrate_related_fields.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_known_keys_meta.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_lenient_related_fields_validators.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_module_api.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_module_api_json_fields.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_module_registry.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_module_settings.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_module_settings_render.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_module_settings_serialize.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_registration.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_accessor.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_api.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_default_clears_override.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_domain_errors.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_env_unprefixed.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_events.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_field_sources.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_framework_reexports.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_group_metadata.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_host_restart_flags.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_module.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_schemas.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_secret_masking.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_secret_urls.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_service.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_typed_secret_masking.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_settings_view_authz.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_store.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_store_filters.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_store_secret_masking.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_tenant_settings.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_testable_checks.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests/test_upload_key_delete.py +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests-js/JsonFields.test.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests-js/ModulesEdit.test.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests-js/RestartHint.test.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests-js/ScopeTabs.test.tsx +0 -0
- {simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_settings
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.41
|
|
4
4
|
Summary: Runtime settings UI — modules plug their own settings panels into a shared admin view
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -21,9 +21,9 @@ Classifier: Topic :: Internet :: WWW/HTTP
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.12
|
|
24
|
-
Requires-Dist: simple-module-core==0.0.
|
|
25
|
-
Requires-Dist: simple-module-db==0.0.
|
|
26
|
-
Requires-Dist: simple-module-hosting==0.0.
|
|
24
|
+
Requires-Dist: simple-module-core==0.0.41
|
|
25
|
+
Requires-Dist: simple-module-db==0.0.41
|
|
26
|
+
Requires-Dist: simple-module-hosting==0.0.41
|
|
27
27
|
Requires-Dist: typer<0.28,>=0.12
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_settings"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.41"
|
|
4
4
|
description = "Runtime settings UI — modules plug their own settings panels into a shared admin view"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -21,9 +21,9 @@ classifiers = [
|
|
|
21
21
|
"Typing :: Typed",
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
|
-
"simple_module_core==0.0.
|
|
25
|
-
"simple_module_db==0.0.
|
|
26
|
-
"simple_module_hosting==0.0.
|
|
24
|
+
"simple_module_core==0.0.41",
|
|
25
|
+
"simple_module_db==0.0.41",
|
|
26
|
+
"simple_module_hosting==0.0.41",
|
|
27
27
|
"typer>=0.12,<0.28",
|
|
28
28
|
]
|
|
29
29
|
|
{simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/endpoints/module_api.py
RENAMED
|
@@ -12,7 +12,6 @@ from typing import Any
|
|
|
12
12
|
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request, Response, status
|
|
13
13
|
from pydantic import ValidationError
|
|
14
14
|
from simple_module_core.module_settings import module_settings_registry
|
|
15
|
-
from simple_module_core.settings_hydrate import hydrate_settings
|
|
16
15
|
from simple_module_hosting.i18n_deps import TranslatorDep
|
|
17
16
|
from simple_module_hosting.permissions import RequiresPermission
|
|
18
17
|
|
|
@@ -23,8 +22,8 @@ from settings._module_settings import (
|
|
|
23
22
|
)
|
|
24
23
|
from settings._module_settings_props import serialize
|
|
25
24
|
from settings.constants import PERM_DELETE, PERM_EDIT, PERM_VIEW
|
|
26
|
-
from settings.contracts.events import SettingsReloaded
|
|
27
25
|
from settings.deps import get_setting_service
|
|
26
|
+
from settings.invalidation import reload_on_commit
|
|
28
27
|
from settings.reload import EnvPinnedError, apply_changes_and_reload
|
|
29
28
|
from settings.service import SettingService
|
|
30
29
|
from settings.store import SettingsStore
|
|
@@ -162,11 +161,6 @@ async def clear_module_field(
|
|
|
162
161
|
store = SettingsStore(service)
|
|
163
162
|
await store.clear_override(package, field)
|
|
164
163
|
|
|
165
|
-
|
|
166
|
-
services = getattr(request.app.state, package)
|
|
167
|
-
services.settings = hydrated
|
|
168
|
-
|
|
169
|
-
bus = request.app.state.sm.event_bus
|
|
170
|
-
await bus.publish(SettingsReloaded(package=package, changed=(field,)))
|
|
164
|
+
reload_on_commit(request.app, request.app.state.sm.event_bus, store, package)
|
|
171
165
|
|
|
172
166
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Rehydrate module settings and notify local consumers only after durable writes."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from simple_module_core.app_state import get_services
|
|
7
|
+
from simple_module_core.events import EventBus
|
|
8
|
+
from simple_module_core.invalidation import InvalidationBus
|
|
9
|
+
from simple_module_core.invalidation_refresh import InvalidationRefresh
|
|
10
|
+
from simple_module_core.module_settings import module_settings_registry
|
|
11
|
+
from simple_module_core.settings_hydrate import hydrate_settings
|
|
12
|
+
from simple_module_db.stored_settings import settings_table_exists
|
|
13
|
+
|
|
14
|
+
from settings.contracts.events import SettingsReloaded
|
|
15
|
+
from settings.store import SettingsStore
|
|
16
|
+
|
|
17
|
+
CHANNEL = "settings.modules"
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def reload_package(app: FastAPI, bus: EventBus, store: SettingsStore, package: str) -> None:
|
|
22
|
+
cls = module_settings_registry(app).get(package)
|
|
23
|
+
if cls is None:
|
|
24
|
+
return
|
|
25
|
+
services = getattr(app.state, package, None)
|
|
26
|
+
if services is None:
|
|
27
|
+
return
|
|
28
|
+
hydrated = await hydrate_settings(cls, store, package, lenient=True)
|
|
29
|
+
changed = tuple(
|
|
30
|
+
sorted(
|
|
31
|
+
name
|
|
32
|
+
for name in cls.model_fields
|
|
33
|
+
if getattr(services.settings, name) != getattr(hydrated, name)
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
services.settings = hydrated
|
|
37
|
+
if changed:
|
|
38
|
+
await bus.publish(SettingsReloaded(package=package, changed=changed))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def reload_on_commit(app: FastAPI, bus: EventBus, store: SettingsStore, package: str) -> None:
|
|
42
|
+
async def committed() -> None:
|
|
43
|
+
invalidation = getattr(get_services(app), "invalidation", None)
|
|
44
|
+
if invalidation is None:
|
|
45
|
+
await reload_package(app, bus, store, package)
|
|
46
|
+
else:
|
|
47
|
+
await invalidation.publish(CHANNEL, key=package)
|
|
48
|
+
|
|
49
|
+
store.on_commit(committed)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def subscribe(bus: InvalidationBus, app: FastAPI) -> InvalidationRefresh:
|
|
53
|
+
from settings.service import SettingService
|
|
54
|
+
|
|
55
|
+
async def reload(key: str | None) -> None:
|
|
56
|
+
async with app.state.sm.db.session_factory() as db:
|
|
57
|
+
if not await settings_table_exists(db): # fresh install, before migrations
|
|
58
|
+
return
|
|
59
|
+
store = SettingsStore(SettingService(db))
|
|
60
|
+
packages = [key] if key is not None else module_settings_registry(app).all_packages()
|
|
61
|
+
for package in packages:
|
|
62
|
+
# One package's failed read or invalid cross-field rule must not
|
|
63
|
+
# starve the packages after it on every later cycle.
|
|
64
|
+
try:
|
|
65
|
+
await reload_package(app, app.state.sm.event_bus, store, package)
|
|
66
|
+
except Exception:
|
|
67
|
+
logger.exception("Reloading %s settings failed; keeping current ones", package)
|
|
68
|
+
await db.rollback()
|
|
69
|
+
|
|
70
|
+
return InvalidationRefresh(bus, CHANNEL, reload)
|
|
@@ -29,6 +29,7 @@ from settings.constants import (
|
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
if TYPE_CHECKING:
|
|
32
|
+
from simple_module_core.invalidation import InvalidationBus
|
|
32
33
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
33
34
|
|
|
34
35
|
|
|
@@ -79,6 +80,19 @@ class SettingsModule(ModuleBase):
|
|
|
79
80
|
# Self-register so the UI lists our own settings alongside other modules.
|
|
80
81
|
services.module_registry.register(MODULE_PACKAGE, SettingsSettings)
|
|
81
82
|
|
|
83
|
+
def register_invalidations(self, bus: InvalidationBus, app: FastAPI) -> None:
|
|
84
|
+
from settings.invalidation import subscribe
|
|
85
|
+
|
|
86
|
+
self._refresh = subscribe(bus, app)
|
|
87
|
+
|
|
88
|
+
async def on_startup(self, app: FastAPI) -> None:
|
|
89
|
+
if hasattr(self, "_refresh"):
|
|
90
|
+
self._refresh.start()
|
|
91
|
+
|
|
92
|
+
async def on_shutdown(self, app: FastAPI) -> None:
|
|
93
|
+
if hasattr(self, "_refresh"):
|
|
94
|
+
await self._refresh.stop()
|
|
95
|
+
|
|
82
96
|
def register_exception_handlers(self, app: FastAPI) -> None:
|
|
83
97
|
from settings.errors import install_exception_handlers
|
|
84
98
|
|
{simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/settings/pages/ModulesEdit.tsx
RENAMED
|
@@ -142,7 +142,11 @@ function ModulesEdit({ modules, testable = {} }: Props) {
|
|
|
142
142
|
</div>
|
|
143
143
|
</Card>
|
|
144
144
|
) : current ? (
|
|
145
|
-
<ModuleForm
|
|
145
|
+
<ModuleForm
|
|
146
|
+
key={current.package}
|
|
147
|
+
module={current}
|
|
148
|
+
checks={testable[current.package] ?? []}
|
|
149
|
+
/>
|
|
146
150
|
) : (
|
|
147
151
|
<p className="text-muted-foreground">{t(keys.settings.modules.empty_title)}</p>
|
|
148
152
|
)}
|
|
@@ -55,10 +55,10 @@ export function FieldSource({ field, onReset }: { field: FieldMeta; onReset: ()
|
|
|
55
55
|
const showDefault = !isBlank(field.default) && String(field.default) !== String(field.value);
|
|
56
56
|
return (
|
|
57
57
|
// Wraps rather than clips: `SM_FILE_STORAGE_QUOTA_BYTES` has no space to
|
|
58
|
-
// break at,
|
|
59
|
-
// `
|
|
58
|
+
// break at, and as an unbreakable token it would set the column's minimum
|
|
59
|
+
// width; `overflow-wrap:anywhere` lets it break inside the identifier.
|
|
60
60
|
<span
|
|
61
|
-
className="block
|
|
61
|
+
className="block [overflow-wrap:anywhere] font-mono text-[11.5px] leading-snug text-muted-foreground"
|
|
62
62
|
title={
|
|
63
63
|
field.env_readable
|
|
64
64
|
? t(keys.settings.modules.env_var_hint)
|
|
@@ -16,10 +16,11 @@ interface Props {
|
|
|
16
16
|
/**
|
|
17
17
|
* One settings row: label, control, trailing meta.
|
|
18
18
|
*
|
|
19
|
-
* Three columns
|
|
20
|
-
*
|
|
21
|
-
* this value from?" in the same place, so the exceptions stand out
|
|
22
|
-
*
|
|
19
|
+
* Three columns when the editor card has room, because the trailing column is
|
|
20
|
+
* what makes the screen readable at a glance — every row answers "where is
|
|
21
|
+
* this value from?" in the same place, so the exceptions stand out. The tracks
|
|
22
|
+
* follow the card's width (container query), not the viewport: two sidebars
|
|
23
|
+
* can leave a narrow card on a wide screen, and the row stacks instead.
|
|
23
24
|
*/
|
|
24
25
|
export function ModuleFieldRow({
|
|
25
26
|
field,
|
|
@@ -37,15 +38,14 @@ export function ModuleFieldRow({
|
|
|
37
38
|
const showDescription = field.description && field.type !== 'bool';
|
|
38
39
|
|
|
39
40
|
return (
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
<div className="grid gap-3 sm:grid-cols-[minmax(170px,auto)_1fr] lg:grid-cols-[minmax(170px,auto)_1fr_210px] lg:items-center">
|
|
41
|
+
// Query the editor card: two sidebars can leave little space at a wide viewport.
|
|
42
|
+
<div className="grid grid-cols-1 gap-3 @md:grid-cols-[minmax(0,140px)_minmax(180px,1fr)] @xl:grid-cols-[minmax(0,140px)_minmax(180px,1fr)_minmax(0,160px)] @xl:items-center">
|
|
43
43
|
<div className="min-w-0">
|
|
44
|
-
<label htmlFor={id} className="block
|
|
44
|
+
<label htmlFor={id} className="block [overflow-wrap:anywhere] text-[13px] font-medium">
|
|
45
45
|
{field.name}
|
|
46
46
|
</label>
|
|
47
47
|
{showDescription && (
|
|
48
|
-
<p className="mt-0.5 text-[11.5px] leading-snug text-muted-foreground">
|
|
48
|
+
<p className="mt-0.5 [overflow-wrap:anywhere] text-[11.5px] leading-snug text-muted-foreground">
|
|
49
49
|
{field.description}
|
|
50
50
|
</p>
|
|
51
51
|
)}
|
|
@@ -65,7 +65,7 @@ export function ModuleFieldRow({
|
|
|
65
65
|
</fieldset>
|
|
66
66
|
</div>
|
|
67
67
|
|
|
68
|
-
<div className="
|
|
68
|
+
<div className="min-w-0 [overflow-wrap:anywhere] @md:col-span-2 @xl:col-span-1 @xl:justify-self-start">
|
|
69
69
|
<FieldSource field={field} onReset={onReset} />
|
|
70
70
|
</div>
|
|
71
71
|
|
|
@@ -75,7 +75,7 @@ export function ModuleFieldRow({
|
|
|
75
75
|
{error && (
|
|
76
76
|
<p
|
|
77
77
|
data-testid={`field-error-${field.name}`}
|
|
78
|
-
className="
|
|
78
|
+
className="min-w-0 [overflow-wrap:anywhere] text-xs text-destructive @md:col-span-1 @md:col-start-2 @xl:col-span-2"
|
|
79
79
|
>
|
|
80
80
|
{error}
|
|
81
81
|
</p>
|
|
@@ -42,18 +42,23 @@ export function ModuleForm({ module: m, checks = [] }: Props) {
|
|
|
42
42
|
return o;
|
|
43
43
|
}, [m.fields]);
|
|
44
44
|
|
|
45
|
-
const [values,
|
|
46
|
-
const [baseline, setBaseline] = useState<Record<string, unknown>>(initial);
|
|
45
|
+
const [{ values, baseline }, setBuffer] = useState({ values: initial, baseline: initial });
|
|
47
46
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
48
47
|
const [busy, setBusy] = useState(false);
|
|
49
48
|
const [saved, setSaved] = useState(false);
|
|
50
49
|
const [formErrors, setFormErrors] = useState<string[]>([]);
|
|
51
50
|
|
|
52
|
-
//
|
|
53
|
-
// switch, or server-reloaded props after a save/reset).
|
|
51
|
+
// Refresh saved values without discarding drafts in other fields (#637).
|
|
54
52
|
useEffect(() => {
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
setBuffer((prev) => ({
|
|
54
|
+
values: Object.fromEntries(
|
|
55
|
+
Object.entries(initial).map(([name, value]) => [
|
|
56
|
+
name,
|
|
57
|
+
notEqual(prev.values[name], prev.baseline[name]) ? prev.values[name] : value,
|
|
58
|
+
]),
|
|
59
|
+
),
|
|
60
|
+
baseline: initial,
|
|
61
|
+
}));
|
|
57
62
|
setErrors({});
|
|
58
63
|
setSaved(false);
|
|
59
64
|
}, [initial]);
|
|
@@ -107,7 +112,7 @@ export function ModuleForm({ module: m, checks = [] }: Props) {
|
|
|
107
112
|
} else {
|
|
108
113
|
// Keeping the form mounted lets the confirmation remain visible instead
|
|
109
114
|
// of being discarded by an immediate Inertia reload.
|
|
110
|
-
|
|
115
|
+
setBuffer((prev) => ({ ...prev, baseline: { ...values } }));
|
|
111
116
|
setSaved(true);
|
|
112
117
|
}
|
|
113
118
|
setBusy(false);
|
|
@@ -121,11 +126,15 @@ export function ModuleForm({ module: m, checks = [] }: Props) {
|
|
|
121
126
|
params: { path: { package: m.package, field: name } },
|
|
122
127
|
}),
|
|
123
128
|
);
|
|
129
|
+
setBuffer((prev) => ({
|
|
130
|
+
...prev,
|
|
131
|
+
values: { ...prev.values, [name]: prev.baseline[name] },
|
|
132
|
+
}));
|
|
124
133
|
router.reload({ only: ['modules'] });
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
return (
|
|
128
|
-
<div className="flex flex-1 flex-col gap-4">
|
|
137
|
+
<div className="flex min-w-0 flex-1 flex-col gap-4">
|
|
129
138
|
{/* Outside the card: the heading names the pane, and the deck keeps the
|
|
130
139
|
card for the fields alone. */}
|
|
131
140
|
<header className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
@@ -169,7 +178,7 @@ export function ModuleForm({ module: m, checks = [] }: Props) {
|
|
|
169
178
|
</div>
|
|
170
179
|
)}
|
|
171
180
|
|
|
172
|
-
<Card className="flex flex-1 flex-col gap-4 border-border p-6">
|
|
181
|
+
<Card className="@container flex min-w-0 flex-1 flex-col gap-4 border-border p-6">
|
|
173
182
|
{/* No inner scroller: the card grows and the page scrolls, which is
|
|
174
183
|
one scrollbar for the screen instead of three nested ones. */}
|
|
175
184
|
<div className="flex flex-col gap-4">
|
|
@@ -187,7 +196,10 @@ export function ModuleForm({ module: m, checks = [] }: Props) {
|
|
|
187
196
|
onReset={() => onReset(f.name)}
|
|
188
197
|
onChange={(name, v) => {
|
|
189
198
|
setSaved(false);
|
|
190
|
-
|
|
199
|
+
setBuffer((prev) => ({
|
|
200
|
+
...prev,
|
|
201
|
+
values: { ...prev.values, [name]: v },
|
|
202
|
+
}));
|
|
191
203
|
}}
|
|
192
204
|
/>
|
|
193
205
|
))}
|
|
@@ -3,7 +3,7 @@ import { Button } from '@simple-module-py/ui/components/ui/button';
|
|
|
3
3
|
import { useRelativeTime } from '@simple-module-py/ui/hooks/use-relative-time';
|
|
4
4
|
import { apiFetch } from '@simple-module-py/ui/lib/api';
|
|
5
5
|
import { CheckCircle2, PlugZap, XCircle } from 'lucide-react';
|
|
6
|
-
import { useEffect, useState } from 'react';
|
|
6
|
+
import { useEffect, useRef, useState } from 'react';
|
|
7
7
|
import { ROUTES } from '../routes';
|
|
8
8
|
import { type LastTest, readLastTest, writeLastTest } from './last-test';
|
|
9
9
|
|
|
@@ -35,9 +35,19 @@ export function TestConnectionButton({ pkg, checks }: Props) {
|
|
|
35
35
|
const [error, setError] = useState<string | null>(null);
|
|
36
36
|
const [last, setLast] = useState<LastTest | null>(null);
|
|
37
37
|
|
|
38
|
+
const requestId = useRef(0);
|
|
39
|
+
|
|
38
40
|
// Read on mount rather than during render: the result is per-tab session
|
|
39
41
|
// state, and touching sessionStorage while rendering breaks SSR hydration.
|
|
40
|
-
useEffect(() =>
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setLast(readLastTest(pkg));
|
|
44
|
+
setResults(null);
|
|
45
|
+
setError(null);
|
|
46
|
+
setBusy(false);
|
|
47
|
+
return () => {
|
|
48
|
+
requestId.current++;
|
|
49
|
+
};
|
|
50
|
+
}, [pkg]);
|
|
41
51
|
|
|
42
52
|
// "users.mailer" → "mailer". Naming the check is the difference between
|
|
43
53
|
// "Test connection" and knowing an SMTP dial is about to happen; with two
|
|
@@ -50,6 +60,7 @@ export function TestConnectionButton({ pkg, checks }: Props) {
|
|
|
50
60
|
: t(keys.settings.modules.test_connection_generic);
|
|
51
61
|
|
|
52
62
|
async function run() {
|
|
63
|
+
const id = ++requestId.current;
|
|
53
64
|
setBusy(true);
|
|
54
65
|
setError(null);
|
|
55
66
|
setResults(null);
|
|
@@ -57,8 +68,9 @@ export function TestConnectionButton({ pkg, checks }: Props) {
|
|
|
57
68
|
const result = await apiFetch<{ checks?: CheckResult[] }>(ROUTES.testConnection(pkg), {
|
|
58
69
|
method: 'POST',
|
|
59
70
|
});
|
|
60
|
-
if (
|
|
61
|
-
|
|
71
|
+
if (id !== requestId.current) return;
|
|
72
|
+
const checkResults = result.data?.checks;
|
|
73
|
+
if (result.ok && Array.isArray(checkResults) && checkResults.length > 0) {
|
|
62
74
|
setResults(checkResults);
|
|
63
75
|
setLast(
|
|
64
76
|
writeLastTest(
|
|
@@ -11,9 +11,13 @@ from pydantic_core import to_jsonable_python
|
|
|
11
11
|
from pydantic_settings import BaseSettings
|
|
12
12
|
from simple_module_core.events import EventBus
|
|
13
13
|
from simple_module_core.module_settings import module_settings_registry
|
|
14
|
-
from simple_module_core.settings_hydrate import
|
|
14
|
+
from simple_module_core.settings_hydrate import (
|
|
15
|
+
env_supplied_fields,
|
|
16
|
+
hydrate_settings,
|
|
17
|
+
value_type_for_field,
|
|
18
|
+
)
|
|
15
19
|
|
|
16
|
-
from settings.
|
|
20
|
+
from settings.invalidation import reload_on_commit
|
|
17
21
|
from settings.store import SettingsStore
|
|
18
22
|
|
|
19
23
|
|
|
@@ -87,7 +91,7 @@ async def apply_changes_and_reload(
|
|
|
87
91
|
package: str,
|
|
88
92
|
changes: dict[str, Any],
|
|
89
93
|
) -> BaseSettings:
|
|
90
|
-
"""Validate
|
|
94
|
+
"""Validate and persist; reload and publish ``SettingsReloaded`` after commit."""
|
|
91
95
|
registry = module_settings_registry(app)
|
|
92
96
|
cls = registry.get(package)
|
|
93
97
|
if cls is None:
|
|
@@ -97,8 +101,8 @@ async def apply_changes_and_reload(
|
|
|
97
101
|
if unknown:
|
|
98
102
|
raise KeyError(f"Unknown field(s) for {package!r}: {sorted(unknown)}")
|
|
99
103
|
|
|
100
|
-
|
|
101
|
-
current =
|
|
104
|
+
# Include prior writes in this transaction without exposing them to other requests.
|
|
105
|
+
current = await hydrate_settings(cls, store, package, lenient=True)
|
|
102
106
|
# Compare against the *dumped* current value, not the attribute: a field
|
|
103
107
|
# typed as a list of models (branding's ``footer_links``) holds model
|
|
104
108
|
# instances, while ``changes`` carries the plain dicts a DTO dumps to. The
|
|
@@ -118,6 +122,7 @@ async def apply_changes_and_reload(
|
|
|
118
122
|
await store.clear_override(package, name)
|
|
119
123
|
|
|
120
124
|
if not diff:
|
|
125
|
+
reload_on_commit(app, bus, store, package)
|
|
121
126
|
return current
|
|
122
127
|
|
|
123
128
|
# Only a field that would actually change is refused: a client that echoes
|
|
@@ -153,6 +158,5 @@ async def apply_changes_and_reload(
|
|
|
153
158
|
except ValidationError as exc:
|
|
154
159
|
raise _relocate(exc, field_name) from exc
|
|
155
160
|
|
|
156
|
-
|
|
157
|
-
await bus.publish(SettingsReloaded(package=package, changed=tuple(sorted(diff))))
|
|
161
|
+
reload_on_commit(app, bus, store, package)
|
|
158
162
|
return validated
|
|
@@ -18,6 +18,7 @@ see ``_module_settings._field_view``.
|
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
+
from simple_module_db.callbacks import OnCommitCallback
|
|
21
22
|
from simple_module_db.stored_settings import ( # noqa: F401 (re-exported, #500)
|
|
22
23
|
SYSTEM_SCOPE_ID,
|
|
23
24
|
get_overrides_sync,
|
|
@@ -41,6 +42,9 @@ class SettingsStore:
|
|
|
41
42
|
def __init__(self, service: SettingService) -> None:
|
|
42
43
|
self._service = service
|
|
43
44
|
|
|
45
|
+
def on_commit(self, callback: OnCommitCallback) -> None:
|
|
46
|
+
self._service.db.on_commit(callback)
|
|
47
|
+
|
|
44
48
|
async def get_overrides(self, package: str) -> dict[str, tuple[str, str]]:
|
|
45
49
|
"""Return ``{field_name: (raw_value, value_type)}`` for a package."""
|
|
46
50
|
items = await self._service.list_by_scope_unmasked(SettingScope.SYSTEM, SYSTEM_SCOPE_ID)
|
|
@@ -14,6 +14,7 @@ from settings.settings import SettingsSettings
|
|
|
14
14
|
from settings.store import SettingsStore
|
|
15
15
|
from simple_module_core.events import EventBus
|
|
16
16
|
from simple_module_core.module_settings import register_module_settings
|
|
17
|
+
from simple_module_db.transaction import finalize_session
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class _UsersCfg(BaseSettings):
|
|
@@ -56,7 +57,10 @@ async def test_apply_changes_updates_app_state_and_fires_event(app_and_bus, db_s
|
|
|
56
57
|
|
|
57
58
|
assert new_settings.allow_signup is True
|
|
58
59
|
assert new_settings.smtp_port == 587
|
|
59
|
-
assert app.state.users.settings is
|
|
60
|
+
assert app.state.users.settings.allow_signup is False
|
|
61
|
+
assert received == []
|
|
62
|
+
await finalize_session(db_session)
|
|
63
|
+
assert app.state.users.settings == new_settings
|
|
60
64
|
assert received == [SettingsReloaded(package="users", changed=("allow_signup", "smtp_port"))]
|
|
61
65
|
|
|
62
66
|
persisted = await store.get_overrides("users")
|
|
@@ -96,3 +100,15 @@ async def test_apply_changes_unknown_package_raises(app_and_bus, db_session):
|
|
|
96
100
|
package="unknown",
|
|
97
101
|
changes={"x": 1},
|
|
98
102
|
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def test_multiple_settings_edits_in_one_transaction_keep_both(app_and_bus, db_session):
|
|
106
|
+
app, bus = app_and_bus
|
|
107
|
+
store = SettingsStore(SettingService(db_session))
|
|
108
|
+
await apply_changes_and_reload(app, bus, store, package="users", changes={"allow_signup": True})
|
|
109
|
+
await apply_changes_and_reload(app, bus, store, package="users", changes={"smtp_port": 587})
|
|
110
|
+
assert app.state.users.settings.allow_signup is False
|
|
111
|
+
assert app.state.users.settings.smtp_port == 25
|
|
112
|
+
await finalize_session(db_session)
|
|
113
|
+
assert app.state.users.settings.allow_signup is True
|
|
114
|
+
assert app.state.users.settings.smtp_port == 587
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Module settings and reload events only reflect committed state."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from settings.contracts.events import SettingsReloaded
|
|
5
|
+
from settings.reload import apply_changes_and_reload
|
|
6
|
+
from settings.service import SettingService
|
|
7
|
+
from settings.store import SettingsStore
|
|
8
|
+
from simple_module_core.invalidation import Invalidation
|
|
9
|
+
from simple_module_db.transaction import finalize_session, rollback_session
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.parametrize("commit", [False, True])
|
|
13
|
+
async def test_settings_and_events_wait_for_commit(app, commit):
|
|
14
|
+
received = []
|
|
15
|
+
bus = app.state.sm.event_bus
|
|
16
|
+
|
|
17
|
+
async def record(event):
|
|
18
|
+
received.append(event)
|
|
19
|
+
|
|
20
|
+
bus.subscribe(SettingsReloaded, record)
|
|
21
|
+
original = app.state.users.settings
|
|
22
|
+
wanted = not original.allow_signup
|
|
23
|
+
async with app.state.sm.db.session_factory() as db:
|
|
24
|
+
await apply_changes_and_reload(
|
|
25
|
+
app,
|
|
26
|
+
bus,
|
|
27
|
+
SettingsStore(SettingService(db)),
|
|
28
|
+
package="users",
|
|
29
|
+
changes={"allow_signup": wanted},
|
|
30
|
+
)
|
|
31
|
+
assert app.state.users.settings is original
|
|
32
|
+
assert received == []
|
|
33
|
+
if commit:
|
|
34
|
+
await finalize_session(db)
|
|
35
|
+
else:
|
|
36
|
+
await rollback_session(db)
|
|
37
|
+
assert app.state.users.settings.allow_signup is (wanted if commit else original.allow_signup)
|
|
38
|
+
assert len(received) == int(commit)
|
|
39
|
+
if commit:
|
|
40
|
+
assert received[0].changed == ("allow_signup",)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def test_remote_settings_changes_and_clears_reload_and_emit_event(app):
|
|
44
|
+
received = []
|
|
45
|
+
|
|
46
|
+
async def record(event):
|
|
47
|
+
received.append(event)
|
|
48
|
+
|
|
49
|
+
app.state.sm.event_bus.subscribe(SettingsReloaded, record)
|
|
50
|
+
original = app.state.users.settings.allow_signup
|
|
51
|
+
factory = app.state.sm.db.session_factory
|
|
52
|
+
async with factory() as db:
|
|
53
|
+
await SettingsStore(SettingService(db)).set_override(
|
|
54
|
+
"users", "allow_signup", str(not original), "bool"
|
|
55
|
+
)
|
|
56
|
+
await db.commit()
|
|
57
|
+
message = Invalidation(channel="settings.modules", key="users", origin="other")
|
|
58
|
+
await app.state.sm.invalidation.deliver(message.to_wire())
|
|
59
|
+
assert app.state.users.settings.allow_signup is not original
|
|
60
|
+
assert received[-1].changed == ("allow_signup",)
|
|
61
|
+
async with factory() as db:
|
|
62
|
+
await SettingsStore(SettingService(db)).clear_override("users", "allow_signup")
|
|
63
|
+
await db.commit()
|
|
64
|
+
await app.state.sm.invalidation.deliver(message.to_wire())
|
|
65
|
+
assert app.state.users.settings.allow_signup is original
|
|
66
|
+
assert len(received) == 2
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.parametrize("commit", [False, True])
|
|
70
|
+
async def test_clearing_module_setting_waits_for_commit(app, commit):
|
|
71
|
+
from settings.endpoints.module_api import clear_module_field
|
|
72
|
+
from starlette.requests import Request
|
|
73
|
+
|
|
74
|
+
original = app.state.users.settings.allow_signup
|
|
75
|
+
factory = app.state.sm.db.session_factory
|
|
76
|
+
async with factory() as db:
|
|
77
|
+
await SettingsStore(SettingService(db)).set_override(
|
|
78
|
+
"users", "allow_signup", str(not original), "bool"
|
|
79
|
+
)
|
|
80
|
+
await db.commit()
|
|
81
|
+
await app.state.sm.invalidation.publish("settings.modules", key="users")
|
|
82
|
+
received = []
|
|
83
|
+
|
|
84
|
+
async def record(event):
|
|
85
|
+
received.append(event)
|
|
86
|
+
|
|
87
|
+
app.state.sm.event_bus.subscribe(SettingsReloaded, record)
|
|
88
|
+
async with factory() as db:
|
|
89
|
+
await clear_module_field(
|
|
90
|
+
"users", "allow_signup", Request({"type": "http", "app": app}), SettingService(db)
|
|
91
|
+
)
|
|
92
|
+
assert app.state.users.settings.allow_signup is not original
|
|
93
|
+
assert received == []
|
|
94
|
+
if commit:
|
|
95
|
+
await finalize_session(db)
|
|
96
|
+
else:
|
|
97
|
+
await rollback_session(db)
|
|
98
|
+
assert app.state.users.settings.allow_signup is (original if commit else not original)
|
|
99
|
+
assert len(received) == int(commit)
|
|
@@ -25,6 +25,13 @@ class _SecretCfg(BaseSettings):
|
|
|
25
25
|
class _Store:
|
|
26
26
|
def __init__(self) -> None:
|
|
27
27
|
self.rows: dict[str, tuple[str, str]] = {}
|
|
28
|
+
self.callbacks = []
|
|
29
|
+
|
|
30
|
+
def on_commit(self, callback) -> None:
|
|
31
|
+
self.callbacks.append(callback)
|
|
32
|
+
|
|
33
|
+
async def get_overrides(self, package):
|
|
34
|
+
return dict(self.rows)
|
|
28
35
|
|
|
29
36
|
async def set_override(self, package, field, raw, value_type) -> None:
|
|
30
37
|
self.rows[field] = (raw, value_type)
|
{simple_module_settings-0.0.39 → simple_module_settings-0.0.41}/tests-js/ModuleForm.test.tsx
RENAMED
|
@@ -105,7 +105,7 @@ describe('ModuleForm', () => {
|
|
|
105
105
|
// control's cell, where it wrapped a word per line.
|
|
106
106
|
const error = screen.getByText(/item rejected/);
|
|
107
107
|
expect(error.contains(screen.getByLabelText('retention_days'))).toBe(false);
|
|
108
|
-
expect(error.className).toContain('
|
|
108
|
+
expect(error.className).toContain('@xl:col-span-2');
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
test('shows an error whose loc names no field at form level', async () => {
|