Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f4f4a6e
Extend Sample (aka AnalysisRequest) with RetentionPeriod field
xispa Feb 18, 2026
e49738f
Add retention period rules in control panel
xispa Feb 18, 2026
593c18e
Set the retention period when storing sample easier
xispa Feb 18, 2026
97a1eae
Add get_default_retention_period in storage's api
xispa Feb 18, 2026
141d0e9
Add getDefaultStorageRetentionPeriod to AnalysisRequest (patch)
xispa Feb 18, 2026
0f0740d
Clear sample's retention period on recover
xispa Feb 18, 2026
6bf693f
Fix TypeError: Missing 'provides' attribute
xispa Feb 18, 2026
b496908
Add upgrade step
xispa Feb 18, 2026
c30dfd2
Use a UIDReferenceField (and widget) for the selection of services
xispa Feb 18, 2026
dac31a8
Fix TypeError: unhashable type: 'list' in get_default_retention_period
xispa Feb 18, 2026
b00776a
Use a schema.Int for the retention period in control panel
xispa Feb 18, 2026
9bd11b9
Auto-fill default retention period when storing sample(s)
xispa Feb 18, 2026
e529a70
Added compiled js
xispa Feb 18, 2026
277deb9
Display retention expiry date in samples lising (inside storage)
xispa Feb 18, 2026
2f3f44d
Store the Exipry Date instead of the retention period
xispa Feb 18, 2026
601657f
Cleanup
xispa Feb 18, 2026
ef4afef
Added doctest
xispa Feb 18, 2026
4a14c24
Changelog
xispa Feb 18, 2026
579217e
Remove JS Viewlet Manager in favour of standard Resources Viewlet
xispa Feb 20, 2026
8d8aed2
Use select html element instead of referencewidget on service selectors
xispa Feb 20, 2026
849ce45
Merge branch '2.x' of github.com:senaite/senaite.storage into retenti…
xispa Feb 20, 2026
0214d6e
Fix doctest
xispa Feb 20, 2026
7f0b125
Changelog
xispa Feb 20, 2026
d0061ae
Merge branch '2.x' into retention-period
ramonski Feb 21, 2026
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)
------------------

- #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
Expand Down
9 changes: 5 additions & 4 deletions src/senaite/storage/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

<!-- Content -->
<metal:core fill-slot="content-core">
<div id="viewlet-senaite-storage-js" tal:content="structure provider:senaite.storage.js" />
<div id="store-samples-view"
tal:define="portal context/@@plone_portal_state/portal;
container python:view.get_container();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@

<!-- Content -->
<metal:core fill-slot="content-core">
<div id="viewlet-senaite-storage-js" tal:content="structure provider:senaite.storage.js" />
<div id="store-samples-view"
class="row"
tal:define="portal context/@@plone_portal_state/portal;">
Expand Down
53 changes: 20 additions & 33 deletions src/senaite/storage/browser/controlpanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand All @@ -95,7 +82,7 @@ class IRetentionRule(Interface):
)


class IStorageControlPanel(Interface):
class IStorageControlPanel(model.Schema):
"""Control panel Settings for senaite.storage
"""

Expand Down
Loading