diff --git a/doc/MANUAL.md b/doc/MANUAL.md
index 970548e..0592de7 100644
--- a/doc/MANUAL.md
+++ b/doc/MANUAL.md
@@ -39,7 +39,11 @@ This is the manual for version %%VERSION%%.
- [Text](#field_types_text)
- [Password](#field_types_password)
- [File](#field_types_file)
-1. [Dynamic forms](#dynamic_forms)
+1. [Dynamic forms](#dynform)
+ - [Supported dynamic form parts](#dynform_support)
+ - [Dynamic select and radio options](#dynform_selectradio)
+ - [Dynamic fields](#dynform_fields)
+ - [Notes](#dynform_notes)
1. [Output](#output)
- [Output types](#output_types)
- [Exit codes](#output_exitcodes)
@@ -649,7 +653,7 @@ Structurally, they are made up of the following elements:
**Default:** `nobody`.
- **`fields`**: List of fields in the form. Each field is a dictionary.
- **Required**, **List of dictionaries**.
+ **Optional**, **List of dictionaries**.
- **`name`**: The name of the field. This is what is passed as an
environment variable to the callback. **Required**, **String**.
@@ -677,6 +681,11 @@ Structurally, they are made up of the following elements:
- **`...`**: Other options, which depend on the type of field. For
more information, see [Field types](#field_types). **Optional**.
+ - **`fields_from`**: Path to a file or executable from which to read the
+ list of fields. See the **fields** option for information on fields. See
+ the [Dynamic forms](#dynamic_forms) chapter for more information.
+ **Optional**, **String**.
+
- **`users`**: A dictionary of users where the key is the username and the
value is the plain text password. This field is not required. **Dictionary**.
@@ -1043,7 +1052,7 @@ For example:
-## Dynamic Forms
+## Dynamic Forms
Often it won't be enough to have staticly defined fields in forms. For
example, you may want the user to be able to select from a dynamically
@@ -1053,9 +1062,20 @@ Scriptform offers a way to fill in parts of a form definition file dynamically
from extern files or scripts. This is done at runtime when showing or
validating the form, so no restarting of scriptform is required.
-When it's possible to dynamically load parts of a form, the form definition
-haev a `_from` option. For example, normally you'd use the `options` element
-for a `select` field in a form, like so:
+### Supported dynamic form parts
+
+Currently the following parts of form definitions support dynamic form parts:
+
+* `forms.X.fields_from`: Form's fields are read from a file or script.
+* Fields of type `select`. Options for dropdowns are read from a file or
+ script.
+* Fields of type `radio`. Options for radio buttons are read from a file or
+ script.
+
+### Dynamic select and radio options
+
+Normally you'd use the `options` element for a `select` field in a form, like
+so:
...
"fields": [
@@ -1102,19 +1122,71 @@ The script `form_loadcsv_target_db.sh` could look like:
]
END_TEXT
-Notes:
+Dynamic options for radio buttons works exactly the same, except the type
+will be `radio`:
+
+ "type": "select",
+ "options": [
+ ["empty", "Empty database"],
+ ["dev", "Development test database"],
+ ["ua", "Acceptance database"]
+ ]
+
+### Dynamic fields
+
+You can also generate all the fields in a form dynamically. For example, the
+following form definition will read all the fields from `form_dyn_fields.sh`:
+
+ {
+ "name": "dyn_fields",
+ "title": "Dynamic fileds",
+ "description": "All the fields in this form are dynamically read from a script.",
+ "submit_title": "Import",
+ "script": "job_import.sh",
+ "fields_from": "form_dyn_fields.sh"
+ }
+
+The script `form_dyn_fields.sh` could look something like:
+
+ #!/bin/sh
+
+ OPTIONS=$(cat <<'END_HEREDOC'
+ [
+ ["test", "Test DB"],
+ ["acc", "Acc DB"],
+ ["prod", "Prod DB"]
+ ]
+ END_HEREDOC
+ )
+
+ cat << END_TEXT
+ [
+ {
+ "name": "target_db",
+ "title": "Database to import to",
+ "type": "radio",
+ "options": $OPTIONS
+ },
+ {
+ "name": "sql_file",
+ "title": "SQL file",
+ "type": "file"
+ }
+ ]
+ END_TEXT
+
+### Notes
* The executable bit must be set in order for scriptform to execute the file.
Otherwise, the contents of the file is read.
* Executable scripts are *always* executed as the user scriptform runs at!
While its not possible for the user to inject anything into the script, you
should still be careful with what the scripts do.
-
-### Supported dynamic form parts
-
-Currently the following parts of form definitions support dynamic form parts:
-
-* `fields`: `type` = `select`.
+* Dynamic parts of a form are loaded / executed each time the form is
+ referenced, so may be read or executed often. E.g. when loading the form,
+ when listing the forms, when showing the form, when submitting the form,
+ when validatinng the form, etc. You should make sure executables run
+ quickly.
## Output
diff --git a/examples/dynamic_forms/dynamic_forms.json b/examples/dynamic_forms/dynamic_forms.json
index 8c0c4a4..bd4856a 100644
--- a/examples/dynamic_forms/dynamic_forms.json
+++ b/examples/dynamic_forms/dynamic_forms.json
@@ -2,17 +2,17 @@
"title": "Dynamic forms",
"forms": [
{
- "name": "import",
- "title": "Import CSV data",
- "description": "Import CSV into a database",
+ "name": "dyn_options",
+ "title": "Dynamic options",
+ "description": "The values for the target database are dynamically read from a script.",
"submit_title": "Import",
"script": "job_import.sh",
"fields": [
{
"name": "target_db",
- "title": "Database to import to",
+ "title": "Target database to import to",
"type": "radio",
- "options_from": "form_import_target_dbs.sh"
+ "options_from": "form_dyn_options_target_db.sh"
},
{
"name": "sql_file",
@@ -20,6 +20,14 @@
"type": "file"
}
]
+ },
+ {
+ "name": "dyn_fields",
+ "title": "Dynamic fileds",
+ "description": "All the fields in this form are dynamically read from a script.",
+ "submit_title": "Import",
+ "script": "job_import.sh",
+ "fields_from": "form_dyn_fields.sh"
}
]
}
diff --git a/examples/dynamic_forms/form_dyn_fields.sh b/examples/dynamic_forms/form_dyn_fields.sh
new file mode 100755
index 0000000..c69a958
--- /dev/null
+++ b/examples/dynamic_forms/form_dyn_fields.sh
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+OPTIONS=$(cat <<'END_HEREDOC'
+[
+ ["test", "Test DB"],
+ ["acc", "Acc DB"],
+ ["prod", "Prod DB"]
+]
+END_HEREDOC
+)
+
+cat << END_TEXT
+[
+ {
+ "name": "target_db",
+ "title": "Database to import to",
+ "type": "radio",
+ "options": $OPTIONS
+ },
+ {
+ "name": "sql_file",
+ "title": "SQL file",
+ "type": "file"
+ }
+]
+END_TEXT
diff --git a/examples/dynamic_forms/form_import_target_dbs.sh b/examples/dynamic_forms/form_dyn_options_target_db.sh
similarity index 100%
rename from examples/dynamic_forms/form_import_target_dbs.sh
rename to examples/dynamic_forms/form_dyn_options_target_db.sh
diff --git a/examples/dynamic_forms/job_import.sh b/examples/dynamic_forms/job_import.sh
index e589470..f280019 100755
--- a/examples/dynamic_forms/job_import.sh
+++ b/examples/dynamic_forms/job_import.sh
@@ -1,6 +1,5 @@
#!/bin/sh
-
MYSQL_DEFAULTS_FILE="my.cnf"
MYSQL="mysql --defaults-file=$MYSQL_DEFAULTS_FILE"
diff --git a/src/formdefinition.py b/src/formdefinition.py
index 4f1626c..97edc07 100644
--- a/src/formdefinition.py
+++ b/src/formdefinition.py
@@ -20,13 +20,15 @@ class FormDefinition(object):
for validation of the form values.
"""
def __init__(self, name, title, description, fields, script,
- default_value=None, output='escaped', hidden=False,
- submit_title="Submit", allowed_users=None, run_as=None):
+ fields_from=None, default_value=None, output='escaped',
+ hidden=False, submit_title="Submit", allowed_users=None,
+ run_as=None):
self.name = name
self.title = title
self.description = description
self.fields = fields
self.script = script
+ self.fields_from = fields_from
self.default_value = default_value
self.output = output
self.hidden = hidden
@@ -34,7 +36,20 @@ class FormDefinition(object):
self.allowed_users = allowed_users
self.run_as = run_as
- self.validate_field_defs(self.fields)
+ self.validate_field_defs(self.get_fields())
+
+ def get_fields(self):
+ """
+ Return the fields for the form either from statically defined fields in
+ the form definition, or dynamically from an externally executable
+ script.
+ """
+ if self.fields is not None:
+ return self.fields
+ elif self.fields_from is not None:
+ return runscript.from_file(self.fields_from)
+ else:
+ raise ValueError("Missing either 'fields' or 'fields_from' in '{}' form".format(self.name))
def validate_field_defs(self, fields):
"""
@@ -52,7 +67,7 @@ class FormDefinition(object):
"""
Return the field definition for `field_name`.
"""
- for field in self.fields:
+ for field in self.get_fields():
if field['name'] == field_name:
return field
raise KeyError("Unknown field: {0}".format(field_name))
@@ -67,7 +82,7 @@ class FormDefinition(object):
values = form_values.copy()
# First make sure all required fields are there
- for field in self.fields:
+ for field in self.get_fields():
field_required = ('required' in field and
field['required'] is True)
field_missing = (field['name'] not in form_values or
@@ -78,7 +93,7 @@ class FormDefinition(object):
)
# Validate the field values, possible casting them to the correct type.
- for field in self.fields:
+ for field in self.get_fields():
field_name = field['name']
if field_name in errors:
# Skip fields that are required but missing, since they can't
diff --git a/src/scriptform.py b/src/scriptform.py
index 5d997bb..6c637ee 100755
--- a/src/scriptform.py
+++ b/src/scriptform.py
@@ -83,8 +83,9 @@ class ScriptForm(object):
FormDefinition(form_name,
form['title'],
form['description'],
- form['fields'],
+ form.get('fields', None),
script,
+ fields_from=form.get("fields_from", None),
default_value=form.get('default_value', ""),
output=form.get('output', 'escaped'),
hidden=form.get('hidden', False),
diff --git a/src/webapp.py b/src/webapp.py
index 6fb7c1f..60dc1e0 100644
--- a/src/webapp.py
+++ b/src/webapp.py
@@ -165,7 +165,7 @@ def censor_form_values(form_def, form_values):
Remove sensitive field values from form_values dict.
"""
censored_form_values = copy.copy(form_values)
- for field in form_def.fields:
+ for field in form_def.get_fields():
if field['type'] == 'password':
censored_form_values[field['name']] = '********'
return censored_form_values
@@ -374,7 +374,7 @@ class ScriptFormWebApp(RequestHandler):
name=form_def.name,
fields=u''.join(
[render_field(f, errors.get(f['name'], []))
- for f in form_def.fields]
+ for f in form_def.get_fields()]
),
submit_title=form_def.submit_title
)