diff --git a/CHANGES.rst b/CHANGES.rst index 4ed934b..e18169f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 diff --git a/src/senaite/storage/content/storage_root_folder.py b/src/senaite/storage/content/storage_root_folder.py new file mode 100644 index 0000000..420f4aa --- /dev/null +++ b/src/senaite/storage/content/storage_root_folder.py @@ -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 + """ diff --git a/src/senaite/storage/profiles/default/factorytool.xml b/src/senaite/storage/profiles/default/factorytool.xml deleted file mode 100644 index fb87c8a..0000000 --- a/src/senaite/storage/profiles/default/factorytool.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/src/senaite/storage/profiles/default/metadata.xml b/src/senaite/storage/profiles/default/metadata.xml index 49c9130..9853586 100644 --- a/src/senaite/storage/profiles/default/metadata.xml +++ b/src/senaite/storage/profiles/default/metadata.xml @@ -6,7 +6,7 @@ dependencies before installing this add-on own profile. --> - 2703 + 2704 profile-senaite.lims:default diff --git a/src/senaite/storage/profiles/default/types.xml b/src/senaite/storage/profiles/default/types.xml index 3ac9077..f3642cf 100644 --- a/src/senaite/storage/profiles/default/types.xml +++ b/src/senaite/storage/profiles/default/types.xml @@ -11,8 +11,7 @@ is not needed: a file in the types folder is enough. --> - - + diff --git a/src/senaite/storage/profiles/default/types/StorageRootFolder.xml b/src/senaite/storage/profiles/default/types/StorageRootFolder.xml index 4641e29..74ba8ce 100644 --- a/src/senaite/storage/profiles/default/types/StorageRootFolder.xml +++ b/src/senaite/storage/profiles/default/types/StorageRootFolder.xml @@ -1,30 +1,88 @@ - - + - - Samples storage + + Sample Storage + + + senaite_theme/icon/storage - StorageRootFolder - senaite.storage - addStorageRootFolder - False + + + StorageRootFolder + + + string:${folder_url}/++add++StorageRootFolder + + + view + + + True + + True + - + + False - + + view + + + + False - - - - + + cmf.AddPortalContent + + + senaite.storage.content.storage_root_folder.IStorageRootFolderSchema + senaite.storage.content.storage_root_folder.StorageRootFolder + + + + + + + + + + + + + + + + + + - + + + + diff --git a/src/senaite/storage/profiles/uninstall/factorytool.xml b/src/senaite/storage/profiles/uninstall/factorytool.xml deleted file mode 100644 index 912f9e9..0000000 --- a/src/senaite/storage/profiles/uninstall/factorytool.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/senaite/storage/profiles/uninstall/types.xml b/src/senaite/storage/profiles/uninstall/types.xml index 1e2419e..b3af61b 100644 --- a/src/senaite/storage/profiles/uninstall/types.xml +++ b/src/senaite/storage/profiles/uninstall/types.xml @@ -1,8 +1,8 @@ - - - - + + + + diff --git a/src/senaite/storage/setuphandlers.py b/src/senaite/storage/setuphandlers.py index 3075923..dfa3c22 100644 --- a/src/senaite/storage/setuphandlers.py +++ b/src/senaite/storage/setuphandlers.py @@ -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 @@ -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 = [ @@ -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) @@ -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 """ @@ -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 @@ -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): diff --git a/src/senaite/storage/upgrade/v02_07_000.py b/src/senaite/storage/upgrade/v02_07_000.py index a86a871..67a8cdc 100644 --- a/src/senaite/storage/upgrade/v02_07_000.py +++ b/src/senaite/storage/upgrade/v02_07_000.py @@ -38,6 +38,7 @@ "StorageFacility", "StorageContainer", "StorageSamplesContainer", + "StorageRootFolder", ] @@ -343,3 +344,74 @@ def migrate_storage_samples_container_to_dx(src, destination): # change the ID *after* the original object was removed migrator.copy_id(src, target) + + +def migrate_storage_root_folder_to_dx(tool): + """Migrate the storage root folder to DX + """ + logger.info("Convert Storage Root Folder to Dexterity ...") + + # ensure old AT types are flushed first + remove_at_portal_types(tool) + + target_id = tmpID() + portal_type = "StorageRootFolder" + portal = tool.aq_inner.aq_parent + + src = portal._getOb("senaite_storage") + if api.is_dexterity_content(src): + logger.info("Storage Root Folder is already a Dexterity type, exiting") + return + + # run required import steps + tool.runImportStepFromProfile(profile, "typeinfo") + tool.runImportStepFromProfile(profile, "workflow") + + # cretate the new facility + target = createContent(portal_type, id=target_id) + portal._setObject(target_id, target) + target = portal._getOb(target_id) + + # Manually set the fields + # NOTE: always convert string values to unicode for dexterity fields! + target.title = api.safe_unicode(src.Title() or u"") + target.description = api.safe_unicode(src.Description() or u"") + + cb = src.manage_copyObjects(ids=src.objectIds()) + target.manage_pasteObjects(cb) + + # Migrate the contents from AT to DX + migrator = getMultiAdapter( + (src, target), interface=IContentMigrator) + + # copy all (raw) attributes from the source object to the target + migrator.copy_attributes(src, target) + + # copy the UID + migrator.copy_uid(src, target) + + # copy auditlog + migrator.copy_snapshots(src, target) + + # copy creators + migrator.copy_creators(src, target) + + # copy workflow history + migrator.copy_workflow_history(src, target) + + # copy marker interfaces + migrator.copy_marker_interfaces(src, target) + + # copy dates + migrator.copy_dates(src, target) + + # uncatalog the source object + migrator.uncatalog_object(src) + + # delete the old object + migrator.delete_object(src) + + # change the ID *after* the original object was removed + migrator.copy_id(src, target) + + logger.info("Convert Storage Root Folder to Dexterity [DONE]") diff --git a/src/senaite/storage/upgrade/v02_07_000.zcml b/src/senaite/storage/upgrade/v02_07_000.zcml index 87d59a4..8e9c5e7 100644 --- a/src/senaite/storage/upgrade/v02_07_000.zcml +++ b/src/senaite/storage/upgrade/v02_07_000.zcml @@ -2,6 +2,14 @@ xmlns="http://namespaces.zope.org/zope" xmlns:genericsetup="http://namespaces.zope.org/genericsetup"> + +