Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
2.7.0 (unreleased)
------------------

- #2898 Add AllowManualEntry for select result options
- #2928 Fix ASTM consumer boundary bugs (sender shape, instrument cascade, sample fallback)
- #2925 Add 'reattach' workflow transition to re-link detached partitions to their primary
- #2926 Strip surrounding whitespace from analysis unit choices so configuration typos don't break the round-trip
Expand Down
9 changes: 6 additions & 3 deletions src/bika/lims/content/abstractbaseanalysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -721,14 +721,17 @@ def getViewFor(self, instance, idx, subfield,
'ResultOptions',
schemata="Result Options",
type='resultsoptions',
subfields=('ResultValue', 'ResultText'),
subfields=('ResultValue', 'ResultText', 'AllowManualEntry'),
required_subfields=('ResultValue', 'ResultText'),
subfield_labels={'ResultValue': _('Result Value'),
'ResultText': _('Display Value'), },
'ResultText': _('Display Value'),
'AllowManualEntry': _('Allow Manual Entry'), },
subfield_types={'AllowManualEntry': 'boolean'},
subfield_validators={'ResultValue': 'result_options_value_validator',
'ResultText': 'result_options_text_validator'},
subfield_sizes={'ResultValue': 5,
'ResultText': 25,},
'ResultText': 25,
'AllowManualEntry': 8,},
subfield_maxlength={'ResultValue': 5,
'ResultText': 255,},
widget=RecordsWidget(
Expand Down
71 changes: 71 additions & 0 deletions src/senaite/core/browser/modals/analysis/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,64 @@ def get_result_options(self):
)
return options

def is_result_option_manual(self, option):
"""Returns whether the option should allow manual text input
"""
if not option.get("AllowManualEntry"):
value = str(option.get("ResultValue", "")).strip().lower()
text = str(option.get("ResultText", "")).strip().lower()
return value == "other" or text == "other"
return option.get("AllowManualEntry")

def get_result_selected_value(self):
"""Returns the selected option value for select-type results

If the current result is custom free text and a manual-entry option
exists, the first manual-entry option is selected.
"""
result = self.get_result()
if not result:
return ""

options = self.get_result_options()
manual_value = ""
for option in options:
value = option.get("ResultValue", "")

if value == result:
return result

if not manual_value and self.is_result_option_manual(option):
manual_value = value

return manual_value

def get_result_other_text(self):
"""Returns the manual free-text result for select-type results
"""
result = self.get_result()
if not result:
return ""
options = self.get_result_options()
for option in options:
value = option.get("ResultValue", "")
if value == result:
return ""
return result

def is_result_selected_option_manual(self):
"""Returns whether currently selected option allows manual entry
"""
selected = self.get_result_selected_value()
if not selected:
return False
options = self.get_result_options()
for option in options:
value = option.get("ResultValue", "")
if value == selected:
return self.is_result_option_manual(option)
return False

def get_result_values(self):
"""Returns the current result as a list of values

Expand Down Expand Up @@ -474,6 +532,19 @@ def handle_submit(self):

form_data = self.request.form

# Normalize select + manual-entry option before persisting:
# if selected option allows manual text, persist the free text.
result_value = form_data.get("Result")
if result_value is not None:
result_value = str(result_value)
options = self.get_result_options()
selected_opt = filter(
lambda o: o.get("ResultValue", "") == result_value,
options
)
if selected_opt and self.is_result_option_manual(selected_opt[0]):
form_data["Result"] = form_data.get("ResultOther", "")

# Map form field names to AT field names
field_map = [
("DetectionLimitOperand", "DetectionLimitOperand"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@
border-bottom-right-radius: 0;
border-right: 0;
}
.edit-analysis-form .result-other-input {
max-width: 100%;
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,47 @@
filterInstruments();
}

/**
* Toggle manual result text input for select result options.
*
* The selected option controls the visibility by the
* "data-manual-entry" option attribute.
*/
function bindResultManualEntry() {
var selects = document.querySelectorAll(
"#edit-analysis-result-select"
);
if (!selects.length) {
return;
}

selects.forEach(function(select) {
var form = select.closest("form");
if (!form) {
return;
}
var input = form.querySelector(
"#edit-analysis-result-other"
);
if (!input) {
return;
}

function toggle() {
var opt = select.options[select.selectedIndex];
var show = !!opt
&& opt.getAttribute("data-manual-entry") === "1";
input.style.display = show ? "" : "none";
if (!show) {
input.value = "";
}
}

select.addEventListener("change", toggle);
toggle();
});
}

// Initialize multiselect-duplicates
document.querySelectorAll(".multiselect-duplicates")
.forEach(function(el) {
Expand All @@ -192,4 +233,7 @@

// Initialize method-instrument filtering
bindMethodInstrumentFilter();

// Initialize manual-entry toggle for select results
bindResultManualEntry();
})();
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,19 @@
<select tal:condition="python:result_type ==
'select'
and result_options"
id="edit-analysis-result-select"
name="Result"
class="custom-select"
tal:define="current view/get_result">
class="custom-select result-select-manual"
tal:define="current view/get_result_selected_value">
<option value=""></option>
<tal:opt repeat="opt result_options">
<option tal:define="
val python:opt.get('ResultValue', '');
txt python:opt.get('ResultText', '')"
txt python:opt.get('ResultText', '');
manual python:view.is_result_option_manual(opt)"
tal:attributes="
value val;
data-manual-entry python:'1' if manual else '0';
selected python:str(val) == str(current)
or None"
tal:content="txt"/>
Expand Down Expand Up @@ -135,6 +138,20 @@

</div>

<!-- Result: manual entry for selected option -->
<input tal:condition="python:result_type ==
'select'
and result_options
and not is_calculated"
type="text"
id="edit-analysis-result-other"
name="ResultOther"
class="form-control form-control-sm mt-2 result-other-input"
tal:define="other view/get_result_other_text;
is_manual view/is_result_selected_option_manual"
tal:attributes="value other;
style python:is_manual and '' or 'display:none;'"/>

<!-- Multi-line result types (outside input-group) -->

<!-- Result: multi-select -->
Expand Down
3 changes: 2 additions & 1 deletion src/senaite/core/exportimport/setupdata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1495,7 +1495,8 @@ def load_result_options(self):
return
sro = service.getResultOptions()
sro.append({'ResultValue': row['ResultValue'],
'ResultText': row['ResultText']})
'ResultText': row['ResultText'],
'AllowManualEntry': row['AllowManualEntry']})
service.setResultOptions(sro)

def load_service_uncertainties(self):
Expand Down
Loading