Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to move containers #22

Merged
merged 11 commits into from
Jan 24, 2021
Merged
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.1.0 (unreleased)
------------------

- #22 Allow to move containers
- #21 Allow storage contents to be deactivated
- #20 Added uninstall profile
- #19 Improved storage listing and structuring with positions
Expand Down
1 change: 1 addition & 0 deletions src/senaite/storage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
# along the path 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.
Expand Down
10 changes: 10 additions & 0 deletions src/senaite/storage/browser/container/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,14 @@
permission="senaite.core.permissions.ManageAnalysisRequests"
layer="senaite.storage.interfaces.ISenaiteStorageLayer" />

<!--
Move containers
-->
<browser:page
for="*"
name="storage_move_container"
class=".move_container.MoveContainerView"
permission="zope.Public"
layer="senaite.storage.interfaces.ISenaiteStorageLayer" />

</configure>
170 changes: 170 additions & 0 deletions src/senaite/storage/browser/container/move_container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# -*- 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(
self.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_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)
return " / ".join(map(self.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": self.get_title(obj),
"path": self.get_container_path(obj),
"url": api.get_url(obj),
"targets": self.get_move_targets_for(obj),
}
96 changes: 96 additions & 0 deletions src/senaite/storage/browser/container/templates/move_container.pt
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
metal:use-macro="here/main_template/macros/master"
i18n:domain="senaite.storage">
<head>
<metal:block fill-slot="senaite_legacy_resources"
tal:define="portal context/@@plone_portal_state/portal;">
<script type="text/javascript"
tal:attributes="src string:${portal/absolute_url}/senaite_widgets/referencewidget.js"></script>
</metal:block>
</head>
<body>

<!-- Title -->
<metal:title fill-slot="content-title">
<h1 i18n:translate="">
Move Containers
</h1>
</metal:title>

<!-- Description -->
<metal:description fill-slot="content-description">
<p i18n:translate="">
<a tal:attributes="href view/back_url"
i18n:name="back_link"
i18n:translate="">
&larr; Back
</a>
</p>
</metal:description>

<!-- Content -->
<metal:core fill-slot="content-core">
<div id="move-container-view"
class="row"
tal:define="portal context/@@plone_portal_state/portal;">

<div class="col-sm-12">
<form class="form"
id="move_container_form"
name="move_container_form"
method="POST">

<!-- Hidden Fields -->
<input type="hidden" name="submitted" value="1"/>
<input tal:replace="structure context/@@authenticator/authenticator"/>

<div class="card mb-3"
tal:repeat="container view/get_container_data">

<div class="card-header">
Move container
<a href="#"
tal:attributes="href container/url"
tal:content="container/path">
</a>
to
</div>

<div class="card-body">
<!-- target position/container selection -->
<select class="form-control"
tal:attributes="name string:move.${container/uid}:record:ignore_empty">
<option selected value="" i18n:translate="">Choose container destination...</option>
<option tal:repeat="target container/targets"
tal:attributes="value target/UID">
<span tal:replace="python:view.get_container_path(target)"/>
</option>
</select>
</div>
</div>

<!-- Form Controls -->
<div>
<!-- Move containers -->
<input class="btn btn-success btn-sm"
type="submit"
name="button_move"
i18n:attributes="value"
value="Move Containers"/>
<!-- Cancel -->
<input class="btn btn-secondary btn-sm"
type="submit"
name="button_cancel"
i18n:attributes="value"
value="Cancel"/>
</div>

</form>
</div>

</div>
</metal:core>
</body>
</html>
9 changes: 9 additions & 0 deletions src/senaite/storage/browser/workflow/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,13 @@
provides="bika.lims.interfaces.IWorkflowActionAdapter"
permission="zope.Public" />

<!-- Workflow action "move_container" -->
<adapter
name="workflow_action_move_container"
for="senaite.storage.interfaces.IStorageContent
zope.publisher.interfaces.browser.IBrowserRequest"
factory=".storagecontainer.WorkflowActionMoveContainerAdapter"
provides="bika.lims.interfaces.IWorkflowActionAdapter"
permission="zope.Public" />

</configure>
21 changes: 20 additions & 1 deletion src/senaite/storage/browser/workflow/storagecontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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)
2 changes: 2 additions & 0 deletions src/senaite/storage/permissions.py
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
3 changes: 3 additions & 0 deletions src/senaite/storage/permissions.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
xmlns="http://namespaces.zope.org/zope"
i18n_domain="senaite.core">

<!-- Move Storage Container Permission -->
<permission id="senaite.storage.permissions.MoveContainer" title="senaite.storage: Transition: Move Container"/>

<!-- Store/Recover Sample Permissions -->
<permission id="senaite.storage.permissions.StoreSample" title="senaite.storage: Transition: Store Sample"/>
<permission id="senaite.storage.permissions.RecoverSample" title="senaite.storage: Transition: Recover Sample"/>
Expand Down
6 changes: 6 additions & 0 deletions src/senaite/storage/profiles/default/rolemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@

<permissions>

<permission name="senaite.storage: Transition: Move Container" acquire="False">
<role name="LabClerk"/>
<role name="LabManager"/>
<role name="Manager"/>
</permission>

<!-- Store/Recover Sample Transition Permissions -->
<permission name="senaite.storage: Transition: Store Sample" acquire="False">
<role name="LabClerk"/>
Expand Down
Loading