Skip to content
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.7.0 (unreleased)
------------------

- #52 Migrate Storage Root Folder to DX
- #51 JS->DX compatibility
- #50 Migrate storage samples container to DX
- #49 Migrate storage container to DX
Expand Down
20 changes: 20 additions & 0 deletions src/senaite/storage/content/storage_root_folder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-

from bika.lims.interfaces import IDoNotSupportSnapshots
from plone.supermodel import model
from senaite.core.content.base import Container
from senaite.core.interfaces import IHideActionsMenu
from senaite.storage.interfaces import IStorageRootFolder
from zope.interface import implementer


class IStorageRootFolderSchema(model.Schema):
"""Schema interface
"""


@implementer(IStorageRootFolder, IStorageRootFolderSchema,
IDoNotSupportSnapshots, IHideActionsMenu)
class StorageRootFolder(Container):
"""The storage root container
"""
16 changes: 0 additions & 16 deletions src/senaite/storage/profiles/default/factorytool.xml

This file was deleted.

2 changes: 1 addition & 1 deletion src/senaite/storage/profiles/default/metadata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
dependencies before installing this add-on own profile.
-->
<metadata>
<version>2703</version>
<version>2704</version>
<!-- Be sure to install the following dependencies if not yet installed -->
<dependencies>
<dependency>profile-senaite.lims:default</dependency>
Expand Down
3 changes: 1 addition & 2 deletions src/senaite/storage/profiles/default/types.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
is not needed: a file in the types folder is enough.
-->
<object name="portal_types">
<object name="StorageRootFolder" meta_type="Factory-based Type Information with dynamic views"/>

<object name="StorageRootFolder" meta_type="Dexterity FTI" />
<object name="StorageFacility" meta_type="Dexterity FTI" />
<object name="StoragePosition" meta_type="Dexterity FTI" />
<object name="StorageContainer" meta_type="Dexterity FTI"/>
Expand Down
90 changes: 74 additions & 16 deletions src/senaite/storage/profiles/default/types/StorageRootFolder.xml
Original file line number Diff line number Diff line change
@@ -1,30 +1,88 @@
<?xml version="1.0"?>
<object name="StorageRootFolder"
meta_type="Factory-based Type Information with dynamic views"
<?xml version="1.0" encoding="UTF-8"?>
<object name="StorageRootFolder" meta_type="Dexterity FTI"
i18n:domain="senaite.storage"
xmlns:i18n="http://xml.zope.org/namespaces/i18n">

<!-- Basic metadata -->
<property name="title" i18n:translate="">Samples storage</property>
<!-- Title and Description -->
<property name="title" i18n:translate="">Sample Storage</property>
<property name="description" i18n:translate=""/>

<!-- content-type icon -->
<property name="icon_expr">senaite_theme/icon/storage</property>
<property name="content_meta_type">StorageRootFolder</property>
<property name="product">senaite.storage</property>
<property name="factory">addStorageRootFolder</property>
<property name="global_allow">False</property>

<!-- factory name; usually the same as type name -->
<property name="factory">StorageRootFolder</property>

<!-- URL TALES expression to add an item TTW -->
<property name="add_view_expr">string:${folder_url}/++add++StorageRootFolder</property>

<property name="link_target"/>
<property name="immediate_view">view</property>

<!-- Is this item addable globally, or is it restricted? -->
<property name="global_allow">True</property>

<!-- If we're a container, should we filter addable content types? -->
<property name="filter_content_types">True</property>
<!-- If filtering, what's allowed -->
<property name="allowed_content_types">
<element value="StorageFacility"/>
<element value="StorageFacility" />
</property>

<property name="allow_discussion">False</property>

<!-- View information -->
<!-- what are our available view methods, and what's the default? -->
<property name="default_view">view</property>
<!-- the view methods below will be selectable via the display tab -->
<property name="view_methods">
<element value="view"/>
</property>
<property name="default_view_fallback">False</property>

<!-- Method aliases -->
<alias from="(Default)" to="view"/>
<alias from="view" to="view"/>
<alias from="edit" to="base_edit"/>
<!-- permission required to add an item of this type -->
<property name="add_permission">cmf.AddPortalContent</property>

<!-- Python class for content items of this sort -->
<property name="schema">senaite.storage.content.storage_root_folder.IStorageRootFolderSchema</property>
<property name="klass">senaite.storage.content.storage_root_folder.StorageRootFolder</property>

<!-- Dexterity behaviours for this type -->
<property name="behaviors">
<element value="plone.app.content.interfaces.INameFromTitle"/>
<element value="plone.app.dexterity.behaviors.metadata.IBasic"/>
<element value="plone.app.referenceablebehavior.referenceable.IReferenceable" />
</property>

<!-- Action aliases -->
<alias from="(Default)" to="(dynamic view)"/>
<alias from="edit" to="@@edit"/>
<alias from="sharing" to="@@sharing"/>
<alias from="view" to="(selected layout)"/>

<!-- View -->
<action title="View"
action_id="view"
category="object"
condition_expr=""
description=""
icon_expr=""
link_target=""
url_expr="string:${object_url}"
visible="True">
<permission value="View"/>
</action>

<!-- Actions -->
<!-- Edit -->
<action title="Edit"
action_id="edit"
category="object"
condition_expr=""
description=""
icon_expr=""
link_target=""
url_expr="string:${object_url}/edit"
visible="False">
<permission value="Modify portal content"/>
</action>

</object>
8 changes: 0 additions & 8 deletions src/senaite/storage/profiles/uninstall/factorytool.xml

This file was deleted.

8 changes: 4 additions & 4 deletions src/senaite/storage/profiles/uninstall/types.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0"?>
<object name="portal_types">
<object remove="True" name="StorageRootFolder" meta_type="Factory-based Type Information with dynamic views"/>
<object remove="True" name="StorageContainer" meta_type="Factory-based Type Information with dynamic views"/>
<object remove="True" name="StorageFacility" meta_type="Factory-based Type Information with dynamic views"/>
<object remove="True" name="StorageSamplesContainer" meta_type="Factory-based Type Information with dynamic views"/>
<object remove="True" name="StorageRootFolder" meta_type="Dexterity FTI"/>
<object remove="True" name="StorageContainer" meta_type="Dexterity FTI"/>
<object remove="True" name="StorageFacility" meta_type="Dexterity FTI"/>
<object remove="True" name="StorageSamplesContainer" meta_type="Dexterity FTI"/>
<object remove="True" name="StoragePosition" meta_type="Dexterity FTI" />
</object>
86 changes: 7 additions & 79 deletions src/senaite/storage/setuphandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
from Acquisition import aq_base
from bika.lims import api
from plone import api as ploneapi
from plone.app.dexterity.behaviors.exclfromnav import IExcludeFromNavigation
from Products.CMFCore.permissions import ModifyPortalContent
from Products.CMFPlone.utils import _createObjectByType
from Products.DCWorkflow.Guard import Guard
from senaite.core import permissions
from senaite.core.catalog import SAMPLE_CATALOG
Expand All @@ -36,16 +36,11 @@
from senaite.storage.config import PRODUCT_NAME
from senaite.storage.config import PROFILE_ID

ACTIONS_TO_HIDE = [
# Tuples of (id, folder_id)
# If folder_id is None, assume folder_id is portal
("bika_storagelocations", "bika_setup")
]

SITE_STRUCTURE = [
# Tuples of (portal_type, obj_id, obj_title, parent_path, display_type)
# If parent_path is None, assume folder_id is portal
("StorageRootFolder", "senaite_storage", "Samples storage", None, True)
("StorageRootFolder", "senaite_storage", "Sample storage", None, True)
]

ID_FORMATTING = [
Expand Down Expand Up @@ -215,12 +210,6 @@ def post_install(portal_setup):
# Setup ID Formatting for Storage content types
setup_id_formatting(portal)

# Hide actions
hide_actions(portal)

# Migrate "classic" storage locations
migrate_storage_locations(portal)

# Injects "store" and "recover" transitions into senaite's workflow
setup_workflows(portal)

Expand Down Expand Up @@ -267,67 +256,6 @@ def setup_catalogs(portal):
setup_catalog_mappings(portal, catalog_mappings=CATALOG_MAPPINGS)


def hide_actions(portal):
"""Excludes actions from both navigation portlet and from control_panel
"""
logger.info("Hiding actions ...")
for action_id, folder_id in ACTIONS_TO_HIDE:
if folder_id and folder_id not in portal:
logger.info("{} not found in portal [SKIP]".format(folder_id))
continue
folder = folder_id and portal[folder_id] or portal
hide_action(folder, action_id)


def hide_action(folder, action_id):
logger.info("Hiding {} from {} ...".format(action_id, folder.id))
if action_id not in folder:
logger.info("{} not found in {} [SKIP]".format(action_id, folder.id))
return

item = folder[action_id]
logger.info("Hide {} ({}) from nav bar".format(action_id, item.Title()))
item.setExcludeFromNav(True)

def get_action_index(action_id):
for n, action in enumerate(cp.listActions()):
if action.getId() == action_id:
return n
return -1

logger.info("Hide {} from control_panel".format(action_id))
cp = api.get_tool("portal_controlpanel")
action_index = get_action_index(action_id)
if (action_index == -1):
logger.info("{} not found in control_panel [SKIP]".format(cp.id))
return

actions = cp._cloneActions()
del actions[action_index]
cp._actions = tuple(actions)
cp._p_changed = 1


def migrate_storage_locations(portal):
"""Migrates classic StorageLocation objects to StorageSamplesContainer
"""
logger.info("Migrating classic Storage Locations ...")
query = dict(portal_type="StorageLocation")
brains = api.search(query, "portal_catalog")
if not brains:
logger.info("No Storage Locations found [SKIP]")
return

total = len(brains)
for num, brain in enumerate(brains):
if num % 100 == 0:
logger.info(
"Migrating Storage Locations: {}/{}".format(num, total))
object = api.get_object(brain) # noqa
# XXX: Do we still need this?
# TODO: Migrate old storage locations


def setup_workflows(portal):
"""Injects 'store' and 'recover' transitions into workflow
"""
Expand Down Expand Up @@ -491,9 +419,7 @@ def resolve_parent(parent_path):
.format(api.get_path(parent), obj_id))
obj = parent._getOb(obj_id)
else:
obj = _createObjectByType(portal_type, parent, obj_id)
obj.edit(title=obj_title)
obj.unmarkCreationFlag()
obj = api.create(parent, portal_type, id=obj_id, title=obj_title)

if display:
# Display the object in the nav bar
Expand All @@ -513,8 +439,10 @@ def display_in_nav(obj):
to_display = to_display + (portal_type, )
ploneapi.portal.set_registry_record(registry_id, to_display)

obj.setExcludeFromNav(False)
obj.reindexObject()
nav_exclude = IExcludeFromNavigation(obj, None)
if nav_exclude:
nav_exclude.exclude_from_nav = False
obj.reindexObject(idxs=["exclude_from_nav"])


def reindex_storage_structure(portal):
Expand Down
Loading