Skip to content

Commit

Permalink
Add move_object function in the API (#2546)
Browse files Browse the repository at this point in the history
* Added move_object function in the api

* Add check_constraints

* Add doctest

* Changelog

* Fix acquisition wrapper

* Full import of api.security

---------

Co-authored-by: Ramon Bartl <rb@ridingbytes.com>
  • Loading branch information
xispa and ramonski committed May 15, 2024
1 parent cd4fe94 commit c2c8f4d
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 0 deletions.
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.6.0 (unreleased)
------------------

- #2546 Add move_object function in the API
- #2539 Add User Profile / Password Reset
- #2543 Fix AttributeError for Instrument Adapters
- #2533 Migrate Sample Points to Dexterity
Expand Down
80 changes: 80 additions & 0 deletions src/bika/lims/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import Missing
import six
from AccessControl.PermissionRole import rolesForPermissionOn
from AccessControl.Permissions import copy_or_move as CopyOrMove
from Acquisition import aq_base
from Acquisition import aq_inner
from Acquisition import aq_parent
Expand All @@ -37,6 +38,7 @@
from bika.lims.interfaces import IContact
from bika.lims.interfaces import ILabContact
from DateTime import DateTime
from OFS.event import ObjectWillBeMovedEvent
from plone import api as ploneapi
from plone.api.exc import InvalidParameterError
from plone.app.layout.viewlets.content import ContentHistoryView
Expand Down Expand Up @@ -71,10 +73,12 @@
from zope.annotation.interfaces import IAttributeAnnotatable
from zope.component import getUtility
from zope.component import queryMultiAdapter
from zope.container.contained import notifyContainerModified
from zope.event import notify
from zope.interface import alsoProvides
from zope.interface import directlyProvides
from zope.interface import noLongerProvides
from zope.lifecycleevent import ObjectMovedEvent
from zope.publisher.browser import TestRequest
from zope.schema import getFieldsInOrder
from zope.security.interfaces import Unauthorized
Expand Down Expand Up @@ -304,6 +308,82 @@ def edit(obj, check_permissions=True, **kwargs):
field.set(obj, value)


def move_object(obj, destination, check_constraints=True):
"""Moves the object to the destination folder
This function has the same effect as:
id = obj.getId()
cp = origin.manage_cutObjects(id)
destination.manage_pasteObjects(cp)
but with slightly better performance. The code is mostly grabbed from
OFS.CopySupport.CopyContainer_pasteObjects
:param obj: object to move to destination
:type obj: ATContentType/DexterityContentType/CatalogBrain/UID
:param destination: destination container
:type destination: ATContentType/DexterityContentType/CatalogBrain/UID
:param check_constraints: constraints and permissions must be checked
:type check_constraints: bool
:returns: The moved object
"""
# prevent circular dependencies
from bika.lims.api.security import check_permission

obj = get_object(obj)
destination = get_object(destination)

# make sure the object is not moved into itself
if obj == destination:
raise ValueError("Cannot move object into itself: {}".format(obj))

# do nothing if destination is the same as origin
origin = get_parent(obj)
if origin == destination:
return obj

if check_constraints:

# check origin object has CopyOrMove permission
if not check_permission(CopyOrMove, obj):
raise Unauthorized("Cannot move {}".format(obj))

# check if portal type is allowed in destination object
portal_type = get_portal_type(obj)
pt = get_tool("portal_types")
ti = pt.getTypeInfo(destination)
if not ti.allowType(portal_type):
raise ValueError("Disallowed subobject type: %s" % portal_type)

id = get_id(obj)

# notify that the object will be copied to destination
obj._notifyOfCopyTo(destination, op=1) # noqa

# notify that the object will be moved to destination
notify(ObjectWillBeMovedEvent(obj, origin, id, destination, id))

# effectively move the object from origin to destination
delete(obj, check_permissions=check_constraints, suppress_events=True)
obj = aq_base(obj)
destination._setObject(id, obj, set_owner=0, suppress_events=True) # noqa
obj = destination._getOb(id) # noqa

# since we used "suppress_events=True", we need to manually notify that the
# object was moved and containers modified. This also makes the objects to
# be re-catalogued
notify(ObjectMovedEvent(obj, origin, id, destination, id))
notifyContainerModified(origin)
notifyContainerModified(destination)

# make ownership implicit if possible, so it acquires the permissions from
# the container
obj.manage_changeOwnershipType(explicit=0)

return obj


def uncatalog_object(obj):
"""Un-catalog the object from all catalogs
Expand Down
73 changes: 73 additions & 0 deletions src/senaite/core/tests/doctests/API.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2311,3 +2311,76 @@ Unless we explicitly tell the system to bypass security check:
>>> obj = api.get_object_by_path(path)
>>> api.is_object(obj)
False


Move an object
..............

This function moves an object from its container to another.

Create the source and destination clients:

>>> orig = api.create(portal.clients, "Client", title="Source Client")
>>> dest = api.create(portal.clients, "Client", title="Destination Client")

Create a new contact in the source client:

>>> contact = api.create(orig, "Contact", Firstname="John", Lastname="Wrong")

Move the contact to the destination client:

>>> id = api.get_id(contact)
>>> orig.hasObject(id)
True
>>> dest.hasObject(id)
False
>>> contact
<Contact at /plone/clients/client-5/contact-2>
>>> contact = api.move_object(contact, dest, check_constraints=False)
>>> api.get_parent(contact) == dest
True
>>> dest.hasObject(id)
True
>>> orig.hasObject(id)
False
>>> contact
<Contact at /plone/clients/client-6/contact-2>

It does nothing if destination is the same as the origin:

>>> contact = api.move_object(contact, dest)
>>> dest.hasObject(id)
True

Trying to move the object into itself is not possible:

>>> api.move_object(contact, contact)
Traceback (most recent call last):
[...]
ValueError: Cannot move object into itself: <Contact at contact-2>

Trying to move an object to another folder without permissions is not possible:

>>> contact = api.move_object(contact, orig)
Traceback (most recent call last):
[...]
Unauthorized: Do not have permissions to remove this object

Unless we grant enough permissions to remove the object from origin:

>>> from bika.lims.api.security import grant_permission_for
>>> grant_permission_for(contact, "Delete objects", ["Authenticated"])
>>> contact = api.move_object(contact, orig)
>>> orig.hasObject(id)
True
>>> dest.hasObject(id)
False
>>> contact
<Contact at /plone/clients/client-5/contact-2>

Still, destination container must allow the object's type:

>>> contact = api.move_object(contact, setup)
Traceback (most recent call last):
[...]
ValueError: Disallowed subobject type: Contact

0 comments on commit c2c8f4d

Please sign in to comment.