Skip to content

Commit 5bead9f

Browse files
committed
Use the dexterity machinery in api.content.create
Fixes #439
1 parent f849395 commit 5bead9f

4 files changed

Lines changed: 94 additions & 26 deletions

File tree

news/439.changed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
When creating dexterity object with api.content.create use the dexterity machinery to not have the object renamed after it is initially created.

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def read(*rnames):
3434
"decorator",
3535
"plone.app.uuid",
3636
"plone.app.linkintegrity",
37+
"plone.dexterity",
3738
"plone.uuid",
3839
"setuptools",
3940
"zope.globalrequest",

src/plone/api/content.py

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@
88
from plone.api.validation import required_parameters
99
from plone.app.linkintegrity.exceptions import LinkIntegrityNotificationException
1010
from plone.app.uuid.utils import uuidToObject
11+
from plone.dexterity.fti import DexterityFTI
12+
from plone.dexterity.utils import addContentToContainer
13+
from plone.dexterity.utils import createContent
1114
from plone.uuid.interfaces import IUUID
1215
from Products.CMFCore.WorkflowCore import WorkflowException
16+
from Products.CMFPlone.interfaces import IConstrainTypes
17+
from zExceptions import BadRequest
1318
from zope.component import getMultiAdapter
1419
from zope.component import getSiteManager
1520
from zope.container.interfaces import INameChooser
@@ -18,7 +23,6 @@
1823
from zope.interface import providedBy
1924

2025
import random
21-
import transaction
2226

2327

2428
_marker = []
@@ -27,13 +31,8 @@
2731
@required_parameters("container", "type")
2832
@at_least_one_of("id", "title")
2933
def create(
30-
container=None,
31-
type=None,
32-
id=None,
33-
title=None,
34-
safe_id=False,
35-
**kwargs # NOQA: C816, S101
36-
):
34+
container=None, type=None, id=None, title=None, safe_id=False, **kwargs
35+
): # NOQA: S101
3736
"""Create a new content item.
3837
3938
:param container: [required] Container object in which to create the new
@@ -61,14 +60,39 @@ def create(
6160
:class:`~plone.api.exc.InvalidParameterError`
6261
:Example: :ref:`content-create-example`
6362
"""
64-
# Create a temporary id if the id is not given
65-
content_id = not safe_id and id or str(random.randint(0, 99999999))
63+
fti = portal.get_tool("portal_types").get(type)
6664

6765
if title:
6866
kwargs["title"] = title
6967

7068
try:
71-
container.invokeFactory(type, content_id, **kwargs)
69+
if isinstance(fti, DexterityFTI):
70+
# For dexterity objects we want to not use the invokeFactory
71+
# method because we want to have the id generated by the name chooser
72+
constraints = IConstrainTypes(container, None)
73+
if constraints and fti not in constraints.allowedContentTypes():
74+
raise ValueError(
75+
f"Subobject type disallowed by IConstrainTypes adapter: {type}"
76+
)
77+
if id:
78+
kwargs["id"] = id
79+
content = createContent(type, **kwargs)
80+
81+
if not content.id:
82+
content.id = INameChooser(container).chooseName(title, content)
83+
if not safe_id:
84+
content.id = INameChooser(container).chooseName(
85+
content.id or title, content
86+
)
87+
if id and id != content.id:
88+
raise BadRequest(
89+
"The id you provided conflicts with an existing object or it is reserved"
90+
)
91+
addContentToContainer(container, content)
92+
content_id = content.id
93+
else:
94+
content_id = not safe_id and id or str(random.randint(0, 99999999))
95+
container.invokeFactory(type, content_id, **kwargs)
7296
except UnicodeDecodeError:
7397
# UnicodeDecodeError is a subclass of ValueError,
7498
# so will be swallowed below unless we re-raise it here
@@ -88,20 +112,6 @@ def create(
88112
)
89113

90114
content = container[content_id]
91-
if not id or (safe_id and id):
92-
# Create a new id from title
93-
chooser = INameChooser(container)
94-
derived_id = id or title
95-
new_id = chooser.chooseName(derived_id, content)
96-
# kacee: we must do a partial commit, else the renaming fails because
97-
# the object isn't in the zodb.
98-
# Thus if it is not in zodb, there's nothing to move. We should
99-
# choose a correct id when
100-
# the object is created.
101-
# maurits: tests run fine without this though.
102-
transaction.savepoint(optimistic=True)
103-
content.aq_parent.manage_renameObject(content_id, new_id)
104-
105115
return content
106116

107117

src/plone/api/tests/test_content.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Tests for plone.api.content."""
22

33
from Acquisition import aq_base
4+
from contextlib import AbstractContextManager
5+
from gc import callbacks
46
from OFS.CopySupport import CopyError
57
from OFS.event import ObjectWillBeMovedEvent
68
from OFS.interfaces import IObjectWillBeMovedEvent
@@ -11,6 +13,7 @@
1113
from plone.app.linkintegrity.exceptions import LinkIntegrityNotificationException
1214
from plone.app.textfield import RichTextValue
1315
from plone.indexer import indexer
16+
from plone.namedfile import NamedBlobFile
1417
from plone.uuid.interfaces import IMutableUUID
1518
from plone.uuid.interfaces import IUUIDGenerator
1619
from Products.CMFCore.interfaces import IContentish
@@ -21,6 +24,7 @@
2124
from zope.component import getGlobalSiteManager
2225
from zope.component import getUtility
2326
from zope.container.contained import ContainerModifiedEvent
27+
from zope.event import subscribers
2428
from zope.interface import alsoProvides
2529
from zope.lifecycleevent import IObjectModifiedEvent
2630
from zope.lifecycleevent import IObjectMovedEvent
@@ -30,6 +34,21 @@
3034
import unittest
3135

3236

37+
class EventRecorder(AbstractContextManager):
38+
def __init__(self):
39+
self.events = []
40+
41+
def __enter__(self):
42+
subscribers.append(self.record)
43+
return self.events
44+
45+
def record(self, event):
46+
self.events.append(event.__class__.__name__)
47+
48+
def __exit__(self, *exc_info):
49+
subscribers.remove(self.record)
50+
51+
3352
class TestPloneApiContent(unittest.TestCase):
3453
"""Unit tests for content manipulation using plone.api."""
3554

@@ -66,7 +85,6 @@ def setUp(self):
6685
id="events",
6786
container=self.portal,
6887
)
69-
7088
self.team = api.content.create(
7189
container=self.about,
7290
type="Document",
@@ -252,6 +270,44 @@ def test_create_dexterity(self):
252270
)
253271
self.verify_intids()
254272

273+
def test_create_dexterity_folder_events(self):
274+
"""Check the events that are fired when a folder is created.
275+
Ensure the events fired are consistent whether or not an id is passed
276+
and assert that the object is not moved in the creation process.
277+
"""
278+
container = self.portal
279+
280+
# Create a folder passing an id and record the events
281+
with EventRecorder() as events_id_yes:
282+
foo = api.content.create(
283+
id="foo",
284+
container=container,
285+
title="Bar",
286+
type="Dexterity Folder",
287+
)
288+
289+
self.assertEqual(foo.id, "foo")
290+
291+
# Create a folder not passing an id and record the events
292+
with EventRecorder() as events_id_not:
293+
bar = api.content.create(
294+
container=container,
295+
title="Bar",
296+
type="Dexterity Folder",
297+
)
298+
299+
self.assertEqual(bar.id, "bar")
300+
301+
self.assertListEqual(
302+
events_id_yes, events_id_not, "The events fired should be consistent"
303+
)
304+
305+
self.assertNotIn(
306+
"ObjectMovedEvent",
307+
events_id_yes,
308+
"The object should be created in the proper place",
309+
)
310+
255311
def test_create_content(self):
256312
"""Test create content."""
257313
container = self.portal

0 commit comments

Comments
 (0)