diff --git a/CHANGES.rst b/CHANGES.rst index 3cc8ffb..2bb6405 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,7 @@ Changelog 2.7.0 (unreleased) ------------------ +- #63 Result type-specific controls in retention rules settings - #64 Add 'Storage Expiry Date' column in samples listing, under 'Stored' - #62 Add configurable storage retention period - #61 Added StorageManager and StorageAssistant roles and counterpart groups diff --git a/src/senaite/storage/api.py b/src/senaite/storage/api.py index a093ca5..993d863 100644 --- a/src/senaite/storage/api.py +++ b/src/senaite/storage/api.py @@ -97,9 +97,6 @@ def get_default_retention_period(sample): rules_by_uid = {} for rule in rules: service_uid = rule.get("service", "") - # UIDReferenceField stores values as lists - if isinstance(service_uid, (list, tuple)): - service_uid = service_uid[0] if service_uid else "" if not service_uid: continue rules_by_uid.setdefault(service_uid, []).append(rule) @@ -112,10 +109,14 @@ def get_default_retention_period(sample): service_uid = analysis.getServiceUID() matching_rules = rules_by_uid.get(service_uid, []) result = analysis.getResult() + # Multiselect/multichoice results are stored as JSON arrays. + # api.to_list parses JSON strings and wraps scalars in a list, + # so we can always use `in` for matching. + result_values = api.to_list(result) for rule in matching_rules: rule_result = rule.get("result", "") retention_days = rule.get("retention_days", 0) - if rule_result and rule_result == result: + if rule_result and rule_result in result_values: specific_candidates.append(retention_days) elif not rule_result: general_candidates.append(retention_days) diff --git a/src/senaite/storage/browser/container/templates/store_container.pt b/src/senaite/storage/browser/container/templates/store_container.pt index 0884d8c..d9ba711 100644 --- a/src/senaite/storage/browser/container/templates/store_container.pt +++ b/src/senaite/storage/browser/container/templates/store_container.pt @@ -30,7 +30,6 @@ -
-
diff --git a/src/senaite/storage/browser/controlpanel.py b/src/senaite/storage/browser/controlpanel.py index 9a51cc5..a12d9e1 100644 --- a/src/senaite/storage/browser/controlpanel.py +++ b/src/senaite/storage/browser/controlpanel.py @@ -18,62 +18,49 @@ # Copyright 2019-2024 by it's authors. # Some rights reserved, see README and LICENSE. -from bika.lims import senaiteMessageFactory as _s +from bika.lims import api from plone.app.registry.browser.controlpanel import ControlPanelFormWrapper from plone.app.registry.browser.controlpanel import RegistryEditForm from plone.autoform import directives from plone.supermodel import model from plone.z3cform import layout from senaite.core.catalog import SETUP_CATALOG -from senaite.core.schema import UIDReferenceField from senaite.core.schema.registry import DataGridRow +from senaite.core.schema.vocabulary import to_simple_vocabulary from senaite.core.z3cform.widgets.datagrid import DataGridWidgetFactory -from senaite.core.z3cform.widgets.uidreference import UIDReferenceWidget from senaite.storage import _ from zope import schema from zope.interface import Interface +from zope.interface import provider +from zope.schema.interfaces import IContextSourceBinder -class ControlPanelUIDReferenceWidget(UIDReferenceWidget): - """UIDReferenceWidget for use in control panel DataGrid rows. - - Overrides get_context to return the form context directly, avoiding - the creation of a temporary object which fails for AT types when the - form context is the Plone site root. +@provider(IContextSourceBinder) +def services_vocabulary(context): + """Returns a SimpleVocabulary made of the active AnalysisService objects """ - - def get_context(self): - form = self.get_form() - return getattr(form, "context", None) + catalog = api.get_tool(SETUP_CATALOG) + query = { + "portal_type": "AnalysisService", + "is_active": True, + "sort_on": "sortable_title", + "sort_order": "ascending", + } + brains = catalog(query) + items = [(api.get_uid(br), api.get_title(br)) for br in brains] + return to_simple_vocabulary(items) class IRetentionRule(Interface): """Schema for a single retention period rule row """ - directives.widget( - "service", - ControlPanelUIDReferenceWidget, - catalog=SETUP_CATALOG, - query={ - "portal_type": ["AnalysisService"], - "is_active": True, - "sort_on": "sortable_title", - "sort_order": "ascending", - }, - columns=[ - {"name": "Title", "label": _s("Title")}, - {"name": "getKeyword", "label": _s("Keyword")}, - {"name": "getCategoryTitle", "label": _s("Category")}, - ], - ) - service = UIDReferenceField( + service = schema.Choice( title=_(u"Analysis Service"), description=_( u"The Analysis Service for this rule" ), - allowed_types=("AnalysisService",), - multi_valued=False, + source=services_vocabulary, required=True, ) @@ -95,7 +82,7 @@ class IRetentionRule(Interface): ) -class IStorageControlPanel(Interface): +class IStorageControlPanel(model.Schema): """Control panel Settings for senaite.storage """ diff --git a/src/senaite/storage/browser/static/bundles/senaite.storage.d65426aaeca6aa27cda4.js b/src/senaite/storage/browser/static/bundles/senaite.storage.d65426aaeca6aa27cda4.js new file mode 100644 index 0000000..510f6cc --- /dev/null +++ b/src/senaite/storage/browser/static/bundles/senaite.storage.d65426aaeca6aa27cda4.js @@ -0,0 +1,2 @@ +(()=>{"use strict";var t={669:t=>{t.exports=jQuery}},e={};function i(n){var r=e[n];if(void 0!==r)return r.exports;var o=e[n]={exports:{}};return t[n](o,o.exports,i),o.exports}(()=>{var t=i(669);function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},e(t)}function n(t,e){for(var i=0;i=0))return o.push(e),o.sort(),r=a(i).val(),a(i).find("option").remove(),a.each(o,function(t,e){return a(i).append(new Option(e,e))}),a(i).val(r)})}},{key:"purge_container_position",value:function(t,e){var i;this.debug("StoreSamplesController::purge_container_position:container_uid=".concat(t,", position=").concat(e)),i=this.get_container_position_selects(t),a.each(i,function(t,i){if(a(i).val()!==e)return a(i).find("option[value='"+e+"']").remove()})}},{key:"get_container_position_selects",value:function(t){return this.debug("StoreSamplesController::get_container_position_selects:container_uid=".concat(t)),a("select[container_uid='".concat(t,"']"))}},{key:"fill_container_positions",value:function(t,e){this.debug("StoreSamplesController::fill_container_positions:container_uid=".concat(t)),a(e).find("option").remove(),a(e).attr("original_value",""),a(e).attr("container_uid",t),this.fetch_available_positions(t).done(function(i){var n,r,o,s,l;for(l=this.get_selected_positions(t),r=0,o=(n=this.diff(i,a.makeArray(l))).length;r",{name:n.attr("name"),id:n.attr("id"),class:n.attr("class")});o.append(_("