Skip to content

Commit

Permalink
Added api.copy_object function for both DX and AT types (senaite#2151)
Browse files Browse the repository at this point in the history
* Improve the function for the creation of AT content types

* Fix doctest BatchClientAssignment

* Check permission when editing object unless temporary

* Move the internal import outside of the loop

* Save one reindex

* Explicitly bypass permission check on creation

* Additional doctest snippet to ensure that new objects are properly indexed

* Added `api.copy_object` function for both DX and AT types

Co-authored-by: Ramon Bartl <rb@ridingbytes.com>
  • Loading branch information
xispa and ramonski committed Sep 29, 2022
1 parent 5133315 commit 6cc1a46
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Changelog
2.3.0 (unreleased)
------------------

- #2152 Added `api.copy_object` function for both DX and AT types
- #2151 Improve the creation process of AT content types
- #2151 Added `api.edit` function for both DX and AT types
- #2151 Improve the creation process of AT content types
- #2151 Added a naive edit function in the API
- #2150 Performance: prioritize raw getter for AllowedMethods field
Expand Down
89 changes: 89 additions & 0 deletions src/bika/lims/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# Copyright 2018-2021 by it's authors.
# Some rights reserved, see README and LICENSE.

import copy
import re
from collections import OrderedDict
from datetime import datetime
Expand Down Expand Up @@ -52,6 +53,7 @@
from Products.CMFCore.interfaces import IFolderish
from Products.CMFCore.interfaces import ISiteRoot
from Products.CMFCore.permissions import ModifyPortalContent
from Products.CMFCore.permissions import View
from Products.CMFCore.utils import getToolByName
from Products.CMFCore.WorkflowCore import WorkflowException
from Products.CMFPlone.RegistrationTool import get_member_by_login_name
Expand Down Expand Up @@ -187,6 +189,93 @@ def create(container, portal_type, *args, **kwargs):
return obj


def copy_object(source, container=None, portal_type=None, *args, **kwargs):
"""Creates a copy of the source object. If container is None, creates the
copy inside the same container as the source. If portal_type is specified,
creates a new object of this type, and copies the values from source fields
to the destination object. Field values sent as kwargs have priority over
the field values from source.
:param source: object from which create a copy
:type source: ATContentType/DexterityContentType/CatalogBrain
:param container: destination container
:type container: ATContentType/DexterityContentType/CatalogBrain
:param portal_type: destination portal type
:returns: The new created object
"""
# Prevent circular dependencies
from security import check_permission
# Use same container as source unless explicitly set
source = get_object(source)
if not container:
container = get_parent(source)

# Use same portal type as source unless explicitly set
if not portal_type:
portal_type = get_portal_type(source)

# Extend the fields to skip with defaults
skip = kwargs.pop("skip", [])
skip = set(skip)
skip.update([
"Products.Archetypes.Field.ComputedField",
"UID",
"id",
"allowDiscussion",
"contributors",
"creation_date",
"creators",
"effectiveDate",
"expirationDate",
"language",
"location",
"modification_date",
"rights",
"subject",
])
# Build a dict for complexity reduction
skip = dict([(item, True) for item in skip])

# Update kwargs with the field values to copy from source
fields = get_fields(source)
for field_name, field in fields.items():
# Prioritize field values passed as kwargs
if field_name in kwargs:
continue
# Skip framework internal fields by name
if skip.get(field_name, False):
continue
# Skip fields of non-suitable types
if hasattr(field, "getType") and skip.get(field.getType(), False):
continue
# Skip readonly fields
if getattr(field, "readonly", False):
continue
# Skip non-readable fields
perm = getattr(field, "read_permission", View)
if perm and not check_permission(perm, source):
continue

# do not wake-up objects unnecessarily
if hasattr(field, "getRaw"):
field_value = field.getRaw(source)
elif hasattr(field, "get_raw"):
field_value = field.get_raw(source)
elif hasattr(field, "getAccessor"):
accessor = field.getAccessor(source)
field_value = accessor()
else:
field_value = field.get(source)

# Do a hard copy of value if mutable type
if isinstance(field_value, (list, dict, set)):
field_value = copy.deepcopy(field_value)
kwargs.update({field_name: field_value})

# Create a copy
return create(container, portal_type, *args, **kwargs)


def edit(obj, check_permissions=True, **kwargs):
"""Updates the values of object fields with the new values passed-in
"""
Expand Down
97 changes: 97 additions & 0 deletions src/senaite/core/tests/doctests/API.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1969,3 +1969,100 @@ portal_factory:
>>> tmp_client = portal.clients.restrictedTraverse(tmp_path)
>>> api.is_temporary(tmp_client)
True

Copying content
...............

This function helps to do it right and copies an existing content for you.

Here we create a copy of the `Client` we created earlier::

>>> client.setTaxNumber('VAT12345')
>>> client2 = api.copy_object(client, title="Test Client 2")
>>> client2
<Client at /plone/clients/client-2>

>>> client2.Title()
'Test Client 2'

>>> client2.getTaxNumber()
'VAT12345'

We can override source values on copy as well::

>>> client.setBankName('Peanuts Bank Ltd')
>>> client3 = api.copy_object(client, title="Test Client 3",
... BankName="Nuts Bank Ltd")
>>> client3
<Client at /plone/clients/client-3>

>>> client3.Title()
'Test Client 3'

>>> client3.getTaxNumber()
'VAT12345'

>>> client3.getBankName()
'Nuts Bank Ltd'

We can create a copy in a container other than source's::

>>> sample_points = self.portal.bika_setup.bika_samplepoints
>>> sample_point = api.create(sample_points, "SamplePoint", title="Test")
>>> sample_point
<SamplePoint at /plone/bika_setup/bika_samplepoints/samplepoint-1>

>>> sample_point_copy = api.copy_object(sample_point, container=client3)
>>> sample_point_copy
<SamplePoint at /plone/clients/client-3/samplepoint-2>

We can even create a copy to a different type::

>>> suppliers = self.portal.bika_setup.bika_suppliers
>>> supplier = api.copy_object(client, container=suppliers,
... portal_type="Supplier", title="Supplier 1")
>>> supplier
<Supplier at /plone/bika_setup/bika_suppliers/supplier-1>

>>> supplier.Title()
'Supplier 1'

>>> supplier.getTaxNumber()
'VAT12345'

>>> supplier.getBankName()
'Peanuts Bank Ltd'

It works for Dexterity types as well::

>>> sample_containers = self.portal.bika_setup.sample_containers
>>> sample_container = api.create(sample_containers, "SampleContainer",
... title="Sample container 4",
... description="Sample container to test",
... capacity="100 ml")
>>> sample_container
<SampleContainer at /plone/bika_setup/sample_containers/samplecontainer-4>

>>> sample_container.Title()
'Sample container 4'

>>> sample_container.Description()
'Sample container to test'

>>> sample_container.getCapacity()
'100 ml'

>>> sample_container_copy = api.copy_object(sample_container,
... title="Sample container 5",
... capacity="50 ml")
>>> sample_container_copy
<SampleContainer at /plone/bika_setup/sample_containers/samplecontainer-5>

>>> sample_container_copy.Title()
'Sample container 5'

>>> sample_container_copy.Description()
'Sample container to test'

>>> sample_container_copy.getCapacity()
'50 ml'

0 comments on commit 6cc1a46

Please sign in to comment.