From 181157987d1ad589dc89883cea49656914d8c1df Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Fri, 22 Jan 2021 20:37:22 +0100 Subject: [PATCH 01/10] Implemented active/inactive states for storage contents --- src/senaite/storage/__init__.py | 4 +- src/senaite/storage/api.py | 8 ++ src/senaite/storage/browser/container/view.py | 8 +- src/senaite/storage/browser/facility/view.py | 8 +- src/senaite/storage/browser/position/view.py | 8 +- src/senaite/storage/browser/storage/view.py | 8 +- src/senaite/storage/config.py | 5 + src/senaite/storage/configure.zcml | 5 +- src/senaite/storage/permissions.py | 7 ++ src/senaite/storage/permissions.zcml | 14 +++ .../storage/profiles/default/rolemap.xml | 30 ++++++ .../definition.xml | 94 +++++++++++-------- src/senaite/storage/setuphandlers.py | 4 +- src/senaite/storage/workflow/__init__.py | 2 - src/senaite/storage/workflow/configure.zcml | 7 ++ .../storage/workflow/storage/__init__.py | 15 +++ .../storage/workflow/storage/events.py | 69 ++++++++++++++ 17 files changed, 243 insertions(+), 53 deletions(-) create mode 100644 src/senaite/storage/config.py create mode 100644 src/senaite/storage/permissions.py create mode 100644 src/senaite/storage/permissions.zcml create mode 100644 src/senaite/storage/profiles/default/rolemap.xml create mode 100644 src/senaite/storage/workflow/storage/__init__.py create mode 100644 src/senaite/storage/workflow/storage/events.py diff --git a/src/senaite/storage/__init__.py b/src/senaite/storage/__init__.py index 85b14bf..383ae7b 100644 --- a/src/senaite/storage/__init__.py +++ b/src/senaite/storage/__init__.py @@ -25,11 +25,9 @@ from Products.Archetypes.atapi import process_types from Products.CMFCore.permissions import AddPortalContent from Products.CMFCore.utils import ContentInit +from senaite.storage.config import PRODUCT_NAME from zope.i18nmessageid import MessageFactory -PRODUCT_NAME = "senaite.storage" -PROFILE_ID = "profile-{}:default".format(PRODUCT_NAME) - # Make senaite.storage modules importable by through-the-web # https://docs.plone.org/develop/plone/security/sandboxing.html # https://docs.zope.org/zope2/zdgbook/Security.html diff --git a/src/senaite/storage/api.py b/src/senaite/storage/api.py index e110146..5b9802e 100644 --- a/src/senaite/storage/api.py +++ b/src/senaite/storage/api.py @@ -20,6 +20,7 @@ from bika.lims import api from senaite.storage.catalog import SENAITE_STORAGE_CATALOG +from senaite.storage.config import STORAGE_WORKFLOW_ID def get_storage_sample(sample_obj_brain_or_uid, as_brain=False): @@ -41,6 +42,13 @@ def get_storage_catalog(): return api.get_tool(SENAITE_STORAGE_CATALOG) +def get_storage_workflow(): + """Returns the storage workflow + """ + wf_tool = api.get_tool("portal_workflow") + return wf_tool.getWorkflowByd(STORAGE_WORKFLOW_ID) + + def get_parents(obj, parents=None, predicate=None): """Return all parents of the object """ diff --git a/src/senaite/storage/browser/container/view.py b/src/senaite/storage/browser/container/view.py index b0d60fd..3d744ab 100644 --- a/src/senaite/storage/browser/container/view.py +++ b/src/senaite/storage/browser/container/view.py @@ -79,7 +79,13 @@ def __init__(self, context, request): }, "confirm_transitions": ["recover_samples"], "columns": self.columns.keys(), - } + }, { + "id": "inactive", + "title": _("Inactive"), + "contentFilter": {"review_state": "inactive"}, + "confirm_transitions": ["recover_samples"], + "columns": self.columns.keys(), + }, ] def folderitem(self, obj, item, index): diff --git a/src/senaite/storage/browser/facility/view.py b/src/senaite/storage/browser/facility/view.py index 94ce00f..5fcf2c5 100644 --- a/src/senaite/storage/browser/facility/view.py +++ b/src/senaite/storage/browser/facility/view.py @@ -80,7 +80,13 @@ def __init__(self, context, request): }, "confirm_transitions": ["recover_samples"], "columns": self.columns.keys(), - } + }, { + "id": "inactive", + "title": _("Inactive"), + "contentFilter": {"review_state": "inactive"}, + "confirm_transitions": ["recover_samples"], + "columns": self.columns.keys(), + }, ] def folderitem(self, obj, item, index): diff --git a/src/senaite/storage/browser/position/view.py b/src/senaite/storage/browser/position/view.py index 8e02037..596d3e7 100644 --- a/src/senaite/storage/browser/position/view.py +++ b/src/senaite/storage/browser/position/view.py @@ -47,7 +47,13 @@ def __init__(self, context, request): }, "confirm_transitions": ["recover_samples"], "columns": self.columns.keys(), - } + }, { + "id": "inactive", + "title": _("Inactive"), + "contentFilter": {"review_state": "inactive"}, + "confirm_transitions": ["recover_samples"], + "columns": self.columns.keys(), + }, ] def isItemAllowed(self, obj): diff --git a/src/senaite/storage/browser/storage/view.py b/src/senaite/storage/browser/storage/view.py index c41be35..560e4da 100644 --- a/src/senaite/storage/browser/storage/view.py +++ b/src/senaite/storage/browser/storage/view.py @@ -88,7 +88,13 @@ def __init__(self, context, request): }, "confirm_transitions": ["recover_samples"], "columns": self.columns.keys(), - } + }, { + "id": "inactive", + "title": _("Inactive"), + "contentFilter": {"review_state": "inactive"}, + "confirm_transitions": ["recover_samples"], + "columns": self.columns.keys(), + }, ] def folderitem(self, obj, item, index): diff --git a/src/senaite/storage/config.py b/src/senaite/storage/config.py new file mode 100644 index 0000000..915aae6 --- /dev/null +++ b/src/senaite/storage/config.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +PRODUCT_NAME = "senaite.storage" +PROFILE_ID = "profile-{}:default".format(PRODUCT_NAME) +STORAGE_WORKFLOW_ID = "senaite_storage_default_workflow" diff --git a/src/senaite/storage/configure.zcml b/src/senaite/storage/configure.zcml index 9aecba9..c9c61ef 100644 --- a/src/senaite/storage/configure.zcml +++ b/src/senaite/storage/configure.zcml @@ -14,9 +14,12 @@ - + + + + diff --git a/src/senaite/storage/permissions.py b/src/senaite/storage/permissions.py new file mode 100644 index 0000000..31472db --- /dev/null +++ b/src/senaite/storage/permissions.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +TransitionAddSamples = "senaite.storage: Transition: Add Samples" +TransitionRecoverSamples = "senaite.storage: Transition: Recover Samples" + +TransitionDeactivate = "senaite.storage: Transition: Deactivate" +TransitionActivate = "senaite.storage: Transition: Activate" diff --git a/src/senaite/storage/permissions.zcml b/src/senaite/storage/permissions.zcml new file mode 100644 index 0000000..eb2bee3 --- /dev/null +++ b/src/senaite/storage/permissions.zcml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/src/senaite/storage/profiles/default/rolemap.xml b/src/senaite/storage/profiles/default/rolemap.xml new file mode 100644 index 0000000..587d889 --- /dev/null +++ b/src/senaite/storage/profiles/default/rolemap.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/senaite/storage/profiles/default/workflows/senaite_storage_default_workflow/definition.xml b/src/senaite/storage/profiles/default/workflows/senaite_storage_default_workflow/definition.xml index 7125fa0..91362f4 100644 --- a/src/senaite/storage/profiles/default/workflows/senaite_storage_default_workflow/definition.xml +++ b/src/senaite/storage/profiles/default/workflows/senaite_storage_default_workflow/definition.xml @@ -22,58 +22,70 @@ List folder contents - - Storage: Recover all samples + + senaite.storage: Transition: Add Samples + senaite.storage: Transition: Recover Samples + senaite.storage: Transition: Activate + senaite.storage: Transition: Deactivate + + - - - Manager - - - - Analyst - LabManager - Manager - Owner - - - - Analyst - LabManager - Manager - Owner - - - - Analyst - LabManager - Manager - Owner - - - - Analyst - LabManager - Manager - Owner - - - - LabManager - Manager - + + + + + + + + + + + + + + + + + + + + + + + + + + + + Activate + + senaite.storage: Transition: Activate + + + + + + + Deactivate + + senaite.storage: Transition: Deactivate + + + + Add samples + senaite.storage: Transition: Add Samples python:here.guard_add_samples() @@ -84,12 +96,12 @@ i18n:attributes="title"> Recover samples - Storage: Recover all samples + senaite.storage: Transition: Recover Samples python:here.guard_recover_samples() - + Previous transition diff --git a/src/senaite/storage/setuphandlers.py b/src/senaite/storage/setuphandlers.py index 9b83631..91ba3ae 100644 --- a/src/senaite/storage/setuphandlers.py +++ b/src/senaite/storage/setuphandlers.py @@ -27,10 +27,10 @@ from Products.CMFPlone.utils import _createObjectByType from Products.DCWorkflow.Guard import Guard from senaite.core.workflow import SAMPLE_WORKFLOW -from senaite.storage import PRODUCT_NAME -from senaite.storage import PROFILE_ID from senaite.storage import logger from senaite.storage.catalog import SENAITE_STORAGE_CATALOG +from senaite.storage.config import PRODUCT_NAME +from senaite.storage.config import PROFILE_ID ACTIONS_TO_HIDE = [ # Tuples of (id, folder_id) diff --git a/src/senaite/storage/workflow/__init__.py b/src/senaite/storage/workflow/__init__.py index c6466b7..4951d6f 100644 --- a/src/senaite/storage/workflow/__init__.py +++ b/src/senaite/storage/workflow/__init__.py @@ -17,5 +17,3 @@ # # Copyright 2019-2020 by it's authors. # Some rights reserved, see README and LICENSE. - -DEFAULT_WORKFLOW_ID = "senaite_storage_default_workflow" diff --git a/src/senaite/storage/workflow/configure.zcml b/src/senaite/storage/workflow/configure.zcml index f0e2ee7..ff764cb 100644 --- a/src/senaite/storage/workflow/configure.zcml +++ b/src/senaite/storage/workflow/configure.zcml @@ -2,6 +2,13 @@ xmlns="http://namespaces.zope.org/zope" i18n_domain="senaite.storage"> + + + {}".format(repr(obj), toggle)) + setattr(obj, PROGRESS_FLAG, toggle) + + +def event_in_progress(obj): + """Checks if the current object or one of the parents is in progress + """ + if is_obj_in_progress(obj): + return True + parents = get_parents( + obj, predicate=lambda o: IStorageRootFolder.providedBy(o)) + return any(map(is_obj_in_progress, parents)) + + +def is_obj_in_progress(obj): + """Checks the progress flag on the object + """ + return getattr(obj, PROGRESS_FLAG, False) From d9c5c938f6ad5fd0f85cf0104853ffc523470a2b Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Fri, 22 Jan 2021 20:52:06 +0100 Subject: [PATCH 02/10] Added transition permissions --- src/senaite/storage/permissions.py | 3 +++ src/senaite/storage/permissions.zcml | 5 ++++- .../storage/profiles/default/rolemap.xml | 18 ++++++++++++++++-- .../definition.xml | 1 + src/senaite/storage/setuphandlers.py | 4 ++-- 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/senaite/storage/permissions.py b/src/senaite/storage/permissions.py index 31472db..de24466 100644 --- a/src/senaite/storage/permissions.py +++ b/src/senaite/storage/permissions.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- +TransitionStoreSample = "senaite.storage: Transition: Store Sample" +TransitionRecoverSample = "senaite.storage: Transition: Recover Sample" + TransitionAddSamples = "senaite.storage: Transition: Add Samples" TransitionRecoverSamples = "senaite.storage: Transition: Recover Samples" diff --git a/src/senaite/storage/permissions.zcml b/src/senaite/storage/permissions.zcml index eb2bee3..ebceee2 100644 --- a/src/senaite/storage/permissions.zcml +++ b/src/senaite/storage/permissions.zcml @@ -2,6 +2,10 @@ xmlns="http://namespaces.zope.org/zope" i18n_domain="senaite.core"> + + + + @@ -10,5 +14,4 @@ - diff --git a/src/senaite/storage/profiles/default/rolemap.xml b/src/senaite/storage/profiles/default/rolemap.xml index 587d889..0fa8c61 100644 --- a/src/senaite/storage/profiles/default/rolemap.xml +++ b/src/senaite/storage/profiles/default/rolemap.xml @@ -3,7 +3,19 @@ - + + + + + + + + + + + + + @@ -15,12 +27,14 @@ - + + + diff --git a/src/senaite/storage/profiles/default/workflows/senaite_storage_default_workflow/definition.xml b/src/senaite/storage/profiles/default/workflows/senaite_storage_default_workflow/definition.xml index 91362f4..68aa5a9 100644 --- a/src/senaite/storage/profiles/default/workflows/senaite_storage_default_workflow/definition.xml +++ b/src/senaite/storage/profiles/default/workflows/senaite_storage_default_workflow/definition.xml @@ -49,6 +49,7 @@ + diff --git a/src/senaite/storage/setuphandlers.py b/src/senaite/storage/setuphandlers.py index 91ba3ae..457bed9 100644 --- a/src/senaite/storage/setuphandlers.py +++ b/src/senaite/storage/setuphandlers.py @@ -175,7 +175,7 @@ "new_state": "stored", "action": "Store sample", "guard": { - "guard_permissions": "", + "guard_permissions": "senaite.storage: Transition: Store Sample", # noqa "guard_roles": "", "guard_expr": "", } @@ -187,7 +187,7 @@ "new_state": "stored", "action": "Recover sample", "guard": { - "guard_permissions": "", + "guard_permissions": "senaite.storage: Transition: Recover Sample", # noqa "guard_roles": "", "guard_expr": "", } From 927b1d304c215a6271b59cd0a6f5652d7a93d037 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Fri, 22 Jan 2021 20:58:14 +0100 Subject: [PATCH 03/10] Updated changelog --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index cc56a9e..8b611ae 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,7 @@ Changelog 2.1.0 (unreleased) ------------------ +- #21 Allow storage contents to be deactivated - #19 Improved storage listing and structuring with positions From 20d8ddd2f72ac39be8c588bddd0dcf362c302c72 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Fri, 22 Jan 2021 21:31:41 +0100 Subject: [PATCH 04/10] Skip already transitioned childs --- src/senaite/storage/workflow/storage/events.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/senaite/storage/workflow/storage/events.py b/src/senaite/storage/workflow/storage/events.py index f3975e7..6888297 100644 --- a/src/senaite/storage/workflow/storage/events.py +++ b/src/senaite/storage/workflow/storage/events.py @@ -14,6 +14,8 @@ def after_activate(obj): def activate_children(children): for child in children: + if api.is_active(child): + continue logger.info("*** Activating {} ***".format(api.get_id(child))) api.do_transition_for(child, "activate") activate_children(child.objectValues()) @@ -33,6 +35,8 @@ def after_deactivate(obj): def deactivate_children(children): for child in children: + if not api.is_active(child): + continue logger.info("*** Deactivating {} ***".format(api.get_id(child))) api.do_transition_for(child, "deactivate") deactivate_children(child.objectValues()) From 4c9acf6d40486b302f8f1c009c77dadff2e5a8ec Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Sat, 23 Jan 2021 15:42:53 +0100 Subject: [PATCH 05/10] Implemented move transition --- src/senaite/storage/__init__.py | 1 + .../storage/browser/container/configure.zcml | 10 ++ .../browser/container/move_container.py | 163 ++++++++++++++++++ .../container/templates/move_container.pt | 96 +++++++++++ .../storage/browser/workflow/configure.zcml | 9 + .../browser/workflow/storagecontainer.py | 21 ++- src/senaite/storage/permissions.py | 2 + src/senaite/storage/permissions.zcml | 3 + .../storage/profiles/default/rolemap.xml | 6 + .../definition.xml | 19 +- .../storage/workflow/storage/guards.py | 21 +++ .../zope_scripts/guard_move_container.py | 12 ++ 12 files changed, 359 insertions(+), 4 deletions(-) create mode 100644 src/senaite/storage/browser/container/move_container.py create mode 100644 src/senaite/storage/browser/container/templates/move_container.pt create mode 100644 src/senaite/storage/workflow/storage/guards.py create mode 100644 src/senaite/storage/zope_scripts/guard_move_container.py diff --git a/src/senaite/storage/__init__.py b/src/senaite/storage/__init__.py index 383ae7b..b555c27 100644 --- a/src/senaite/storage/__init__.py +++ b/src/senaite/storage/__init__.py @@ -37,6 +37,7 @@ # used to access the module. Thus, all the modules from the path passed in to # `allow_module` will be available. allow_module('senaite.storage.workflow.samplescontainer.guards') +allow_module('senaite.storage.workflow.storage.guards') # Defining a Message Factory for when this product is internationalized. diff --git a/src/senaite/storage/browser/container/configure.zcml b/src/senaite/storage/browser/container/configure.zcml index 21fb7fc..689219f 100644 --- a/src/senaite/storage/browser/container/configure.zcml +++ b/src/senaite/storage/browser/container/configure.zcml @@ -42,4 +42,14 @@ permission="senaite.core.permissions.ManageAnalysisRequests" layer="senaite.storage.interfaces.ISenaiteStorageLayer" /> + + + diff --git a/src/senaite/storage/browser/container/move_container.py b/src/senaite/storage/browser/container/move_container.py new file mode 100644 index 0000000..7d32c52 --- /dev/null +++ b/src/senaite/storage/browser/container/move_container.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +from bika.lims import api +from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile +from senaite.storage import logger +from senaite.storage import senaiteMessageFactory as _ +from senaite.storage.api import get_parents +from senaite.storage.browser import BaseView +from senaite.storage.catalog import SENAITE_STORAGE_CATALOG +from senaite.storage.interfaces import IStorageFacility +from senaite.storage.interfaces import IStorageContainer +from senaite.storage.interfaces import IStorageSamplesContainer + + +class MoveContainerView(BaseView): + """Allows to move containers in other facilities/positions/containers + """ + template = ViewPageTemplateFile("templates/move_container.pt") + action = "storage_move_container" + + def __init__(self, context, request): + super(MoveContainerView, self).__init__(context, request) + self.context = context + self.request = request + self.portal = api.get_portal() + self.back_url = "{}/senaite_storage".format(api.get_url(self.portal)) + + def __call__(self): + form = self.request.form + + # Form submit toggle + form_submitted = form.get("submitted", False) + form_move = form.get("button_move", False) + form_cancel = form.get("button_cancel", False) + + # No items selected + if not self.get_objects(): + return self.redirect( + message=_("No items selected"), level="warning") + + # Handle store + if form_submitted and form_move: + logger.info("*** MOVE CONTAINER ***") + move = form.get("move", {}) + for src, dest in move.items(): + self.move_container(src, dest) + return self.redirect() + + # Handle cancel + if form_submitted and form_cancel: + return self.redirect(message=_("Container moving cancelled")) + + return self.template() + + def get_objects(self): + """Get the objects to be moved + """ + # fetch objects from request + objs = self.get_objects_from_request() + if objs: + return objs + if IStorageSamplesContainer.providedBy(self.context): + return [self.context] + if IStorageContainer.providedBy(self.context): + return [self.context] + return None + + def add_status_message(self, message, level="info"): + """Set a portal status message + """ + return self.context.plone_utils.addPortalMessage(message, level) + + def move_container(self, src, dest): + """move container from source to destination + """ + source = api.get_object(src) + destination = api.get_object(dest) + parent = api.get_parent(source) + + if destination == parent: + message = _(u"Container {} is already located in destination path!" + .format(self.get_container_path(source))) + self.add_status_message(message, level="warning") + return False + cb = parent.manage_cutObjects(ids=[api.get_id(source)]) + destination.manage_pasteObjects(cb_copy_data=cb) + message = _(u"Moved container {} → {}".format( + api.get_title(source), self.get_container_path(destination))) + self.add_status_message(message, level="info") + return True + + def get_parents_for(self, container): + """Get the parent objects for the container + """ + if IStorageFacility.providedBy(container): + return [container] + + def predicate(obj): + return IStorageFacility.providedBy(obj) + + return get_parents(container, predicate=predicate) + + def get_container_path(self, container): + """Return the facility container path + """ + parents = list(reversed(self.get_parents_for(container))) + parents.append(container) + return " / ".join(map(api.get_title, parents)) + + def get_movable_containers(self): + """Get movable containers + + NOTE: contained containers will be omitted + """ + movable_containers = [] + containers = self.get_objects() + for container in containers: + parents = self.get_parents_for(container) + if set(parents).intersection(containers): + continue + movable_containers.append(container) + return movable_containers + + def get_move_targets_for(self, container): + """Get move targets for the given container + """ + targets = [] + container_path = api.get_path(container) + target_types = [ + "StorageFacility", + "StoragePosition", + "StorageContainer", + ] + # sample containers can only be moved inside containers + if IStorageSamplesContainer.providedBy(container): + target_types = ["StorageContainer"] + query = { + "portal_type": target_types, + "review_state": "active" + } + brains = api.search(query, SENAITE_STORAGE_CATALOG) + for brain in brains: + path = api.get_path(brain) + if path.startswith(container_path): + continue + targets.append(api.get_object(brain)) + return targets + + def get_container_data(self): + """Returns a list of containers that can be moved + + """ + for obj in self.get_movable_containers(): + obj = api.get_object(obj) + yield { + "obj": obj, + "id": api.get_id(obj), + "uid": api.get_uid(obj), + "title": api.get_title(obj), + "path": self.get_container_path(obj), + "url": api.get_url(obj), + "targets": self.get_move_targets_for(obj), + } diff --git a/src/senaite/storage/browser/container/templates/move_container.pt b/src/senaite/storage/browser/container/templates/move_container.pt new file mode 100644 index 0000000..0c3056c --- /dev/null +++ b/src/senaite/storage/browser/container/templates/move_container.pt @@ -0,0 +1,96 @@ + + + + + + + + + + +

+ Move Containers +

+
+ + + +

+ + ← Back + +

+
+ + + +
+ +
+
+ + + + + +
+ +
+ Move container + + + to +
+ +
+ + +
+
+ + +
+ + + + +
+ +
+
+ +
+
+ + diff --git a/src/senaite/storage/browser/workflow/configure.zcml b/src/senaite/storage/browser/workflow/configure.zcml index 60a4648..ae98991 100644 --- a/src/senaite/storage/browser/workflow/configure.zcml +++ b/src/senaite/storage/browser/workflow/configure.zcml @@ -23,4 +23,13 @@ provides="bika.lims.interfaces.IWorkflowActionAdapter" permission="zope.Public" /> + + + diff --git a/src/senaite/storage/browser/workflow/storagecontainer.py b/src/senaite/storage/browser/workflow/storagecontainer.py index 3918b1e..771de63 100644 --- a/src/senaite/storage/browser/workflow/storagecontainer.py +++ b/src/senaite/storage/browser/workflow/storagecontainer.py @@ -19,9 +19,10 @@ # Some rights reserved, see README and LICENSE. from bika.lims import api -from senaite.storage.interfaces import IStorageSamplesContainer from bika.lims.browser.workflow import RequestContextAware from bika.lims.interfaces import IWorkflowActionUIDsAdapter +from senaite.storage.interfaces import IStorageLayoutContainer +from senaite.storage.interfaces import IStorageSamplesContainer from zope.interface import implementer @@ -41,3 +42,21 @@ def __call__(self, action, uids): url = "{}/storage_store_container?uids={}".format( self.back_url, ",".join(container_uids)) return self.redirect(redirect_url=url) + + +@implementer(IWorkflowActionUIDsAdapter) +class WorkflowActionMoveContainerAdapter(RequestContextAware): + """Adapter in charge of "move container" action + """ + + def __call__(self, action, uids): + """Redirects the user to the Samples selector view + """ + # filter out UIDs not belonging to sample containers + objs = map(api.get_object, uids) + containers = filter( + lambda o: IStorageLayoutContainer.providedBy(o), objs) + container_uids = map(api.get_uid, containers) + url = "{}/storage_move_container?uids={}".format( + self.back_url, ",".join(container_uids)) + return self.redirect(redirect_url=url) diff --git a/src/senaite/storage/permissions.py b/src/senaite/storage/permissions.py index de24466..c6c68a4 100644 --- a/src/senaite/storage/permissions.py +++ b/src/senaite/storage/permissions.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +TransitionMoveContainer = "senaite.storage: Transition: Move Container" + TransitionStoreSample = "senaite.storage: Transition: Store Sample" TransitionRecoverSample = "senaite.storage: Transition: Recover Sample" diff --git a/src/senaite/storage/permissions.zcml b/src/senaite/storage/permissions.zcml index ebceee2..b6486c1 100644 --- a/src/senaite/storage/permissions.zcml +++ b/src/senaite/storage/permissions.zcml @@ -2,6 +2,9 @@ xmlns="http://namespaces.zope.org/zope" i18n_domain="senaite.core"> + + + diff --git a/src/senaite/storage/profiles/default/rolemap.xml b/src/senaite/storage/profiles/default/rolemap.xml index 0fa8c61..2314252 100644 --- a/src/senaite/storage/profiles/default/rolemap.xml +++ b/src/senaite/storage/profiles/default/rolemap.xml @@ -3,6 +3,12 @@ + + + + + + diff --git a/src/senaite/storage/profiles/default/workflows/senaite_storage_default_workflow/definition.xml b/src/senaite/storage/profiles/default/workflows/senaite_storage_default_workflow/definition.xml index 68aa5a9..590f6dc 100644 --- a/src/senaite/storage/profiles/default/workflows/senaite_storage_default_workflow/definition.xml +++ b/src/senaite/storage/profiles/default/workflows/senaite_storage_default_workflow/definition.xml @@ -34,6 +34,7 @@ + @@ -50,6 +51,7 @@ + @@ -82,9 +84,9 @@ - Add samples + Add samples senaite.storage: Transition: Add Samples python:here.guard_add_samples() @@ -93,7 +95,7 @@ Recover samples @@ -102,6 +104,17 @@ + + + Move container + + senaite.storage: Transition: Move Container + python:here.guard_move_container() + + + Previous transition diff --git a/src/senaite/storage/workflow/storage/guards.py b/src/senaite/storage/workflow/storage/guards.py new file mode 100644 index 0000000..981f558 --- /dev/null +++ b/src/senaite/storage/workflow/storage/guards.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +from AccessControl.SecurityInfo import ModuleSecurityInfo +from bika.lims import api +from senaite.storage.interfaces import IStorageContainer +from senaite.storage.interfaces import IStorageSamplesContainer + +security = ModuleSecurityInfo(__name__) + + +@security.public +def guard_move_container(container): + """Guard for move container + """ + if not api.is_active(container): + return False + if IStorageContainer.providedBy(container): + return True + if IStorageSamplesContainer.providedBy(container): + return True + return False diff --git a/src/senaite/storage/zope_scripts/guard_move_container.py b/src/senaite/storage/zope_scripts/guard_move_container.py new file mode 100644 index 0000000..016ccf7 --- /dev/null +++ b/src/senaite/storage/zope_scripts/guard_move_container.py @@ -0,0 +1,12 @@ +## Script (Python) "guard_move_container" +##bind container=container +##bind context=context +##bind namespace= +##bind script=script +##bind subpath=traverse_subpath +##parameters= +##title= +## + +from senaite.storage.workflow.storage.guards import guard_move_container +return guard_move_container(context) From 2e7b5c7294a4c5a115bc64731e6a0169e36cc929 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Sat, 23 Jan 2021 15:47:18 +0100 Subject: [PATCH 06/10] Allow to move positions as well --- src/senaite/storage/workflow/storage/guards.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/senaite/storage/workflow/storage/guards.py b/src/senaite/storage/workflow/storage/guards.py index 981f558..2bca895 100644 --- a/src/senaite/storage/workflow/storage/guards.py +++ b/src/senaite/storage/workflow/storage/guards.py @@ -3,6 +3,7 @@ from AccessControl.SecurityInfo import ModuleSecurityInfo from bika.lims import api from senaite.storage.interfaces import IStorageContainer +from senaite.storage.interfaces import IStoragePosition from senaite.storage.interfaces import IStorageSamplesContainer security = ModuleSecurityInfo(__name__) @@ -14,6 +15,8 @@ def guard_move_container(container): """ if not api.is_active(container): return False + if IStoragePosition.providedBy(container): + return True if IStorageContainer.providedBy(container): return True if IStorageSamplesContainer.providedBy(container): From 4b238fdd0b261efecc26247e44be3e72e7a18ab3 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Sat, 23 Jan 2021 15:48:38 +0100 Subject: [PATCH 07/10] Reverted last commit --- src/senaite/storage/workflow/storage/guards.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/senaite/storage/workflow/storage/guards.py b/src/senaite/storage/workflow/storage/guards.py index 2bca895..981f558 100644 --- a/src/senaite/storage/workflow/storage/guards.py +++ b/src/senaite/storage/workflow/storage/guards.py @@ -3,7 +3,6 @@ from AccessControl.SecurityInfo import ModuleSecurityInfo from bika.lims import api from senaite.storage.interfaces import IStorageContainer -from senaite.storage.interfaces import IStoragePosition from senaite.storage.interfaces import IStorageSamplesContainer security = ModuleSecurityInfo(__name__) @@ -15,8 +14,6 @@ def guard_move_container(container): """ if not api.is_active(container): return False - if IStoragePosition.providedBy(container): - return True if IStorageContainer.providedBy(container): return True if IStorageSamplesContainer.providedBy(container): From a7d94e39d57c9eb2fb8162d850074f246f21e43b Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Sat, 23 Jan 2021 15:50:47 +0100 Subject: [PATCH 08/10] Changelog updated --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8b611ae..6c39868 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,7 @@ Changelog 2.1.0 (unreleased) ------------------ +- #22 Allow to move containers - #21 Allow storage contents to be deactivated - #19 Improved storage listing and structuring with positions From 0cc18d688c5524a007045c237058310dc3afe318 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Sat, 23 Jan 2021 15:58:47 +0100 Subject: [PATCH 09/10] Handle unicode paths --- src/senaite/storage/browser/container/move_container.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/senaite/storage/browser/container/move_container.py b/src/senaite/storage/browser/container/move_container.py index 7d32c52..35424e2 100644 --- a/src/senaite/storage/browser/container/move_container.py +++ b/src/senaite/storage/browser/container/move_container.py @@ -105,7 +105,8 @@ def get_container_path(self, container): """ parents = list(reversed(self.get_parents_for(container))) parents.append(container) - return " / ".join(map(api.get_title, parents)) + path = " / ".join(map(api.get_title, parents)) + return api.safe_unicode(path) def get_movable_containers(self): """Get movable containers From ae74acedf6f4de77c0c45c2dd6dfdba132913578 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Sat, 23 Jan 2021 16:09:23 +0100 Subject: [PATCH 10/10] More unicode handling --- .../storage/browser/container/move_container.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/senaite/storage/browser/container/move_container.py b/src/senaite/storage/browser/container/move_container.py index 35424e2..e30bec3 100644 --- a/src/senaite/storage/browser/container/move_container.py +++ b/src/senaite/storage/browser/container/move_container.py @@ -84,8 +84,9 @@ def move_container(self, src, dest): return False cb = parent.manage_cutObjects(ids=[api.get_id(source)]) destination.manage_pasteObjects(cb_copy_data=cb) + message = _(u"Moved container {} → {}".format( - api.get_title(source), self.get_container_path(destination))) + self.get_title(source), self.get_container_path(destination))) self.add_status_message(message, level="info") return True @@ -100,13 +101,18 @@ def predicate(obj): return get_parents(container, predicate=predicate) + def get_title(self, obj): + """Return the object title as unicode + """ + title = api.get_title(obj) + return api.safe_unicode(title) + def get_container_path(self, container): """Return the facility container path """ parents = list(reversed(self.get_parents_for(container))) parents.append(container) - path = " / ".join(map(api.get_title, parents)) - return api.safe_unicode(path) + return " / ".join(map(self.get_title, parents)) def get_movable_containers(self): """Get movable containers @@ -157,7 +163,7 @@ def get_container_data(self): "obj": obj, "id": api.get_id(obj), "uid": api.get_uid(obj), - "title": api.get_title(obj), + "title": self.get_title(obj), "path": self.get_container_path(obj), "url": api.get_url(obj), "targets": self.get_move_targets_for(obj),