Skip to content

A module's settings

Your module has choices that are not yours to make. How many days a loan lasts, whether a reminder goes out, which address the mail leaves from: those belong to the customer, not to your code. They have a screen, and that screen is declared in a file.

modules/library/
├── module.json
├── settings.json <- here
└── models/
{
"parameters": [
{ "code": "library.loan_days", "type": "integer", "default": 21 },
{ "code": "library.reminder_enabled", "type": "boolean", "default": true }
],
"groups": [{ "id": "library", "label": "settings.tab.library", "order": 90 }],
"forms": [
{
"key": "library.settings",
"label": "settings.library.title",
"read_groups": ["kws_admin"],
"write_groups": ["kws_admin"],
"settings_group": "library",
"settings_order": 10,
"fields": [
{ "code": "library.loan_days", "label": "settings.library.loan_days" },
{ "code": "library.reminder_enabled", "label": "settings.library.reminder" }
]
}
]
}

Restart: a Library tab appears under Settings, with its two fields and their defaults. You registered nothing, imported nothing, wired nothing.

The three sections

All optional, and each answers a different question.

parameters declares the parameter itself: its code, its type, its default. Five types, no more: string, integer, decimal, boolean, json. The default is validated at boot, so a typo surfaces at startup rather than at the first click.

groups creates a Settings tab. A module with a single form can skip it and sit inside an existing tab.

forms is the screen: a title, who may read it, who may write it, and the fields. A field does not redeclare its type: it points at a code, and the type declared above picks the editor.

Permissions

An empty read_groups means open to read. An empty write_groups never means open to write: a form with no write list is editable by nobody. That is deliberate, and it is the right default: a forgotten setting is read-only, not wide open.

A field can do a little more

{
"code": "mail.provider",
"label": "settings.mail.provider",
"choices": [["smtp", "SMTP (your server)"], ["console", "Console (dev)"]]
},
{
"code": "mail.smtp.host",
"label": "settings.mail.smtp_host",
"show_when": { "field": "mail.provider", "in": ["smtp"] }
},
{
"code": "mail.smtp.password_secret_ref",
"label": "settings.mail.password",
"secret": true
}
  • choices turns the field into a dropdown.
  • show_when shows the field only while another one holds one of the listed values. A hidden field is never required, so it can never block a save.
  • secret: true is the one case where the value does not travel back. What the administrator types goes encrypted into the vault (kewos/secrets, rule 16), the parameter keeps only a reference, and reading the form returns a plain “configured” boolean. An SMTP password never comes back in an HTTP response.

Reading the setting from your code

from kewos.config import get_parameter
days = get_parameter(engine, "library.loan_days") # tenant default
days = get_parameter(engine, "library.loan_days", company_id) # that company's override

Resolution is a three-step cascade, run here on a real instance:

before any write : 21 <- the value declared in the file
after a tenant write : 30
company 1 (inherited) : 30 <- no override: it inherits
company 1 (overridden) : 7
tenant default : 30 <- untouched by the override

So one company can have its own loan length without moving the others, and you wrote no cascade logic at all.

What the file cannot do

A validator. If your parameter must refuse certain values (“at least one day”), JSON cannot say so. You declare it in Python instead, and the file merely references it in a form:

def register(registry):
register_parameter(
"library.loan_days", ParameterType.INTEGER, default=14, validator=_refuse_zero,
)

The kernel reads your settings.json after your register(), precisely for this: your Python declaration wins, and the file’s duplicate is skipped silently. The validator survives.