Skip to content

Commit

Permalink
Improve the creation process of AT content types (senaite#2150)
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
  • Loading branch information
xispa committed Sep 29, 2022
1 parent bb5fa7e commit 5133315
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 15 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Changelog
2.3.0 (unreleased)
------------------

- #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
- #2148 Performance: prioritize raw getter for AllowedInstruments field
- #2147 Remove stale function workflow.getReviewHistory
Expand Down
45 changes: 43 additions & 2 deletions src/bika/lims/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@
from plone.memoize.volatile import DontCache
from Products.Archetypes.atapi import DisplayList
from Products.Archetypes.BaseObject import BaseObject
from Products.Archetypes.event import ObjectInitializedEvent
from Products.Archetypes.utils import mapply
from Products.CMFCore.interfaces import IFolderish
from Products.CMFCore.interfaces import ISiteRoot
from Products.CMFCore.permissions import ModifyPortalContent
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 @@ -153,9 +156,17 @@ def create(container, portal_type, *args, **kwargs):
fti = types_tool.getTypeInfo(portal_type)

if fti.product:
# create the AT object
obj = _createObjectByType(portal_type, container, id)
obj.processForm()
obj.edit(title=title, **kwargs)
# update the object with values
edit(obj, check_permissions=False, title=title, **kwargs)
# auto-id if required
if obj._at_rename_after_creation:
obj._renameAfterCreation(check_auto_id=True)
# we are no longer under creation
obj.unmarkCreationFlag()
# notify that the object was created
notify(ObjectInitializedEvent(obj))
else:
# newstyle factory
factory = getUtility(IFactory, fti.factory)
Expand All @@ -176,6 +187,36 @@ def create(container, portal_type, *args, **kwargs):
return obj


def edit(obj, check_permissions=True, **kwargs):
"""Updates the values of object fields with the new values passed-in
"""
# Prevent circular dependencies
from security import check_permission
fields = get_fields(obj)
for name, value in kwargs.items():
field = fields.get(name, None)
if not field:
continue

# cannot update readonly fields
readonly = getattr(field, "readonly", False)
if readonly:
raise ValueError("Field '{}' is readonly".format(name))

# check field writable permission
if check_permissions:
perm = getattr(field, "write_permission", ModifyPortalContent)
if perm and not check_permission(perm, obj):
raise Unauthorized("Field '{}' is not writeable".format(name))

# Set the value
if hasattr(field, "getMutator"):
mutator = field.getMutator(obj)
mapply(mutator, value)
else:
field.set(obj, value)


def get_tool(name, context=None, default=_marker):
"""Get a portal tool by name
Expand Down
35 changes: 26 additions & 9 deletions src/bika/lims/subscribers/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,10 @@
from Products.CMFPlone.utils import safe_unicode


def ObjectModifiedEventHandler(batch, event):
"""Actions to be done when a batch is created:
- Title as the Batch ID if title is not defined
- Move the Batch inside the Client if defined
def move_to_client(batch):
"""Moves the batch to the client assigned in the Client field if it does
not belong to that client yet. Does nothing otherwise
"""
if not batch.title:
batch.setTitle(safe_unicode(batch.id).encode('utf-8'))

# If client is assigned, move the Batch the client's folder
# Note here we directly get the Client from the Schema, cause getClient
# getter is overriden in Batch content type to always look to aq_parent in
Expand All @@ -39,5 +35,26 @@ def ObjectModifiedEventHandler(batch, event):
# Check if the Batch is being created inside the Client
if client and (client.UID() != batch.aq_parent.UID()):
# move batch inside the client
cp = batch.aq_parent.manage_cutObjects(batch.id)
client.manage_pasteObjects(cp)
if batch.id in batch.aq_parent.objectIds():
cp = batch.aq_parent.manage_cutObjects(batch.id)
client.manage_pasteObjects(cp)


def ObjectInitializedEventHandler(batch, event):
"""Actions to be done when a batch is created:
- Title as the Batch ID if title is not defined
- Move the Batch inside the Client if defined
"""
if not batch.title:
batch.setTitle(safe_unicode(batch.id).encode('utf-8'))

# Try to move the batch to it's client folder
move_to_client(batch)


def ObjectModifiedEventHandler(batch, event):
"""Moves the batch object inside the folder that represents the client this
batch has been assigned to via "Client" field
"""
# Try to move the batch to it's client folder
move_to_client(batch)
6 changes: 5 additions & 1 deletion src/bika/lims/subscribers/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,18 @@
handler="bika.lims.subscribers.analysisrequest.AfterTransitionEventHandler"
/>

<!-- Batch modified event
<!-- Batch initialized and modified events
This subscriber moves a Batch to its corresponding client folder when the
the batch is being created in batches folder and a client is set. This only
happens when the Batch is created by lab personnel, cause batches created by
client contacts are directly created inside their own folder.
Therefore, this behaviour allows client contacts to access to batches that
were created by lab personnel and were assigned to their client.
-->
<subscriber
for="bika.lims.interfaces.IBatch
Products.Archetypes.interfaces.IObjectInitializedEvent"
handler="bika.lims.subscribers.batch.ObjectInitializedEventHandler" />
<subscriber
for="bika.lims.interfaces.IBatch
zope.lifecycleevent.interfaces.IObjectModifiedEvent"
Expand Down
74 changes: 72 additions & 2 deletions src/senaite/core/tests/doctests/API.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,78 @@ Here we create a new `Client` in the `plone/clients` folder::
>>> client
<Client at /plone/clients/client-1>

>>> client.Title()
'Test Client'
>>> client.Title()
'Test Client'

Created objects are properly indexed::

>>> services = self.portal.bika_setup.bika_analysisservices
>>> service = api.create(services, "AnalysisService",
... title="Dummy service", Keyword="DUM")
>>> uid = api.get_uid(service)
>>> catalog = api.get_tool("senaite_catalog_setup")
>>> brains = catalog(portal_type="AnalysisService", UID=uid)
>>> brains[0].getKeyword
'DUM'


Editing Content
...............

This function helps to edit a given content.

Here we update the `Client` we created earlier, an AT::

>>> api.edit(client, AccountNumber="12343567890", BankName="BTC Bank")
>>> client.getAccountNumber()
'12343567890'

>>> client.getBankName()
'BTC Bank'

It also works for DX content types::

>>> api.edit(senaite_setup, site_logo_css="my-test-logo")
>>> senaite_setup.getSiteLogoCSS()
'my-test-logo'

The field need to be writeable::

>>> field = client.getField("BankName")
>>> field.readonly = True
>>> api.edit(client, BankName="Lydian Lion Coins Bank")
Traceback (most recent call last):
[...]
ValueError: Field 'BankName' is readonly

>>> client.getBankName()
'BTC Bank'

>>> field.readonly = False
>>> api.edit(client, BankName="Lydian Lion Coins Bank")
>>> client.getBankName()
'Lydian Lion Coins Bank'

And user need to have enough permissions to change the value as well::

>>> field.write_permission = "Delete objects"
>>> api.edit(client, BankName="Electrum Coins")
Traceback (most recent call last):
[...]
Unauthorized: Field 'BankName' is not writeable

>>> client.getBankName()
'Lydian Lion Coins Bank'

Unless we manually force to bypass the permissions check::

>>> api.edit(client, check_permissions=False, BankName="Electrum Coins")
>>> client.getBankName()
'Electrum Coins'

Restore permission::

>>> field.write_permission = "Modify Portal Content"


Getting a Tool
Expand Down
1 change: 0 additions & 1 deletion src/senaite/core/tests/doctests/BatchClientAssignment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ Client's folder:
If the client is assigned on creation, same behavior as before:

>>> batch = api.create(portal.batches, "Batch", Client=client)
>>> modified(batch)
>>> len(batches.objectValues("Batch"))
0
>>> len(client.objectValues("Batch"))
Expand Down

0 comments on commit 5133315

Please sign in to comment.