Skip to content

Commit 18c05ff

Browse files
committed
add option_given method to BasicProcessor
1 parent eaace32 commit 18c05ff

2 files changed

Lines changed: 66 additions & 5 deletions

File tree

backend/lib/processor.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,16 @@ def work(self):
200200

201201
# build the single runtime view of this dataset's parameters: every
202202
# stored parameter, plus the declared default for any option that was
203-
# not stored. Worker code should read options from self.parameters
204-
# only, and not re-read them from the dataset mid-run — stored
205-
# parameters may change during a run, but this dictionary stays
206-
# complete and stable.
203+
# not stored, so worker code can read any declared option without
204+
# checking whether it exists. The stored parameters themselves are
205+
# kept as self.given_parameters: they hold only the options that were
206+
# actually part of the submission (see option_given()). Worker code
207+
# should read options from self.parameters, and not re-read them from
208+
# the dataset mid-run — stored parameters may change during a run,
209+
# but these dictionaries stay stable.
207210
given_parameters = self.dataset.parameters.copy()
208211
all_parameters = self.get_options(self.source_dataset, config=self.config)
212+
self.given_parameters = given_parameters
209213
self.parameters = {
210214
param: given_parameters.get(param, all_parameters.get(param, {}).get("default"))
211215
for param in [*all_parameters.keys(), *given_parameters.keys()]
@@ -438,6 +442,29 @@ def after_process(self):
438442
except (SMTPException, ConnectionRefusedError, socket.timeout):
439443
self.log.error("Error sending email to %s" % owner)
440444

445+
def option_given(self, option):
446+
"""
447+
Check whether an option was actually part of this dataset's submission
448+
449+
`self.parameters` always contains every option this worker declares,
450+
with defaults filled in for anything that was not stored. That is
451+
convenient for reading values, but it cannot tell you whether the
452+
option was really part of the submission. This method can: it checks
453+
the parameters as they were stored when the dataset was created. For
454+
datasets queued via the web interface, an option is only stored if
455+
the user could see it when submitting (i.e. its `requires` condition
456+
was met); for datasets queued by other code (e.g. presets), an option
457+
is only stored if the calling code explicitly set it.
458+
459+
Note that a stored option may still hold its default value: a user
460+
who saw an option and left it untouched still counts as having been
461+
given it.
462+
463+
:param str option: Option name, as used in `get_options()`
464+
:return bool: Whether the option was part of the stored parameters
465+
"""
466+
return option in self.given_parameters
467+
441468
def clean_up_on_error(self):
442469
try:
443470
# ensure proxied requests are stopped
@@ -1087,7 +1114,8 @@ def validate_query(query, request, config):
10871114
marked `sensitive` are removed from the stored record when the dataset
10881115
starts running. At run time, the worker reads all of this back through
10891116
`self.parameters`, with declared option defaults filled in for any
1090-
missing keys.
1117+
missing keys; `option_given()` tells a worker whether an option was
1118+
really part of the stored submission.
10911119
10921120
By default nothing is checked and the input is stored as-is. Search
10931121
workers must define their own version: doing so is what makes a

tests/test_modules.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,39 @@ def test_option_declarations(logger, fourcat_modules, mock_dataset, mock_basic_c
485485
logger.info("All option declarations look consistent.")
486486

487487

488+
def test_parse_all_gated_options():
489+
"""
490+
Options with a "requires" condition are only part of the parsed input when
491+
their condition is met - whether or not the (hidden) form field was
492+
submitted. This keeps the stored parameters honest: a missing key means
493+
the user never saw the option, a present key means they chose a value or
494+
its default applies. At run time, self.parameters fills every declared
495+
option regardless (so plain reads are always safe); the stored honesty is
496+
exposed to workers through BasicProcessor.option_given().
497+
"""
498+
from common.lib.user_input import UserInput
499+
500+
options = {
501+
"gate": {"type": UserInput.OPTION_TOGGLE, "default": False},
502+
"gated": {"type": UserInput.OPTION_TEXT, "default": ",", "requires": "gate==true"},
503+
"gated_toggle": {"type": UserInput.OPTION_TOGGLE, "default": True, "requires": "gate==true"},
504+
"plain": {"type": UserInput.OPTION_TEXT, "default": "x"},
505+
}
506+
507+
# gate off, gated fields not submitted: gated options are absent, not defaulted
508+
parsed = UserInput.parse_all(options, {"option-plain": "y"})
509+
assert parsed == {"gate": False, "plain": "y"}
510+
511+
# gate off, gated field submitted anyway (e.g. hidden field still posts):
512+
# still absent
513+
parsed = UserInput.parse_all(options, {"option-gated": ";"})
514+
assert "gated" not in parsed and "gated_toggle" not in parsed
515+
516+
# gate on: gated options are parsed (submitted value) or defaulted (absent)
517+
parsed = UserInput.parse_all(options, {"option-gate": "on", "option-gated": ";"})
518+
assert parsed["gated"] == ";" and parsed["gated_toggle"] is False and parsed["gate"] is True
519+
520+
488521
def test_dataset_finish_raises_on_double_finish(mock_dataset):
489522
"""
490523
Regression guard for common/lib/dataset.py:986.

0 commit comments

Comments
 (0)