Skip to content

Commit 65931e2

Browse files
Fix IObjectCreatedEvent firing with temporary ID (#1362)
Move notify(ObjectCreatedEvent) inside add() so it fires after INameChooser assigns the final ID, not before. Fixes the bug where subscribers could not rely on event.object.id for ID-dependent logic such as setting sequential shortnames. Fixes: #1362
1 parent f829714 commit 65931e2

File tree

6 files changed

+56
-14
lines changed

6 files changed

+56
-14
lines changed

news/1362.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix `IObjectCreatedEvent` firing with a temporary object ID when content is created via the REST API. The event now fires after `INameChooser` assigns the final ID, so subscribers can rely on `event.object.id` to set sequential shortnames or perform ID-dependent logic. @bhardwajparth51

src/plone/restapi/services/content/add.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616
from zExceptions import Unauthorized
1717
from zope.component import getMultiAdapter
1818
from zope.component import queryMultiAdapter
19-
from zope.event import notify
2019
from zope.interface import alsoProvides
21-
from zope.lifecycleevent import ObjectCreatedEvent
2220

2321
import plone.protect.interfaces
2422

@@ -92,9 +90,9 @@ def reply(self):
9290
setattr(obj, "_plone.uuid", uid)
9391

9492
if not getattr(deserializer, "notifies_create", False):
95-
notify(ObjectCreatedEvent(obj))
96-
97-
obj = add(self.context, obj, rename=not bool(id_))
93+
obj = add(self.context, obj, rename=not bool(id_), notify_created=True)
94+
else:
95+
obj = add(self.context, obj, rename=not bool(id_))
9896

9997
# Link translation given the translation_of property
10098
if (

src/plone/restapi/services/content/tus.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@
1717
from uuid import uuid4
1818
from zExceptions import Unauthorized
1919
from zope.component import queryMultiAdapter
20-
from zope.event import notify
2120
from zope.interface import implementer
22-
from zope.lifecycleevent import ObjectCreatedEvent
2321
from zope.publisher.interfaces import IPublishTraverse
2422
from zope.publisher.interfaces import NotFound
2523

@@ -281,8 +279,9 @@ def create_or_modify_content(self, tus_upload):
281279

282280
if mode == "create":
283281
if not getattr(deserializer, "notifies_create", False):
284-
notify(ObjectCreatedEvent(obj))
285-
obj = add(self.context, obj)
282+
obj = add(self.context, obj, notify_created=True)
283+
else:
284+
obj = add(self.context, obj)
286285

287286
tus_upload.close()
288287
tus_upload.cleanup()

src/plone/restapi/services/content/utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from zope.container.contained import ObjectAddedEvent
1414
from zope.container.interfaces import INameChooser
1515
from zope.event import notify
16+
from zope.lifecycleevent import ObjectCreatedEvent
1617

1718

1819
def create(container, type_, id_=None, title=None):
@@ -68,13 +69,15 @@ def create(container, type_, id_=None, title=None):
6869
return obj
6970

7071

71-
def add(container, obj, rename=True):
72+
def add(container, obj, rename=True, notify_created=False):
7273
"""Add an object to a container."""
7374
id_ = getattr(aq_base(obj), "id", None)
7475

7576
# Archetypes objects are already created in a container thus we just fire
7677
# the notification events and rename the object if necessary.
7778
if base_hasattr(obj, "_at_rename_after_creation"):
79+
if notify_created:
80+
notify(ObjectCreatedEvent(obj))
7881
notify(ObjectAddedEvent(obj, container, id_))
7982
notifyContainerModified(container)
8083
if obj._at_rename_after_creation and rename:
@@ -92,6 +95,8 @@ def add(container, obj, rename=True):
9295
suggestion = obj.Title()
9396
id_ = chooser.chooseName(suggestion, obj)
9497
obj.id = id_
98+
if notify_created:
99+
notify(ObjectCreatedEvent(obj))
95100
new_id = container._setObject(id_, obj)
96101
# _setObject triggers ObjectAddedEvent which can end up triggering a
97102
# content rule to move the item to a different container. In this case

src/plone/restapi/tests/test_content_post.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,48 @@ def record_event(event):
211211
sm.unregisterHandler(record_event, (IObjectAddedEvent,))
212212
sm.unregisterHandler(record_event, (IObjectModifiedEvent,))
213213

214+
def test_post_to_folder_object_created_event_has_final_id(self):
215+
"""IObjectCreatedEvent must fire with the final, INameChooser-resolved ID.
216+
217+
When no 'id' is supplied, plone.restapi assigns a temporary ID and
218+
renames the object via INameChooser before adding it to the container.
219+
Subscribers must see the final ID (e.g. 'my-sequential-doc') on
220+
event.object.id, not the temporary one (e.g. 'document.2026-03-06.1234').
221+
222+
Regression test for https://github.com/plone/plone.restapi/issues/1362
223+
"""
224+
sm = getGlobalSiteManager()
225+
created_event_ids = []
226+
227+
def capture_id(event):
228+
created_event_ids.append(event.object.id)
229+
230+
sm.registerHandler(capture_id, (IObjectCreatedEvent,))
231+
232+
response = requests.post(
233+
self.portal.folder1.absolute_url(),
234+
headers={"Accept": "application/json"},
235+
auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD),
236+
json={"@type": "Document", "title": "My Sequential Doc"},
237+
)
238+
self.assertEqual(201, response.status_code)
239+
240+
final_id = response.json()["id"]
241+
self.assertEqual(
242+
1, len(created_event_ids), "Expected exactly one ObjectCreatedEvent"
243+
)
244+
245+
event_id = created_event_ids[0]
246+
self.assertEqual(
247+
final_id,
248+
event_id,
249+
f"IObjectCreatedEvent carried '{event_id}' but final ID was '{final_id}'. "
250+
"Subscribers relying on event.object.id will see a stale value. "
251+
"See https://github.com/plone/plone.restapi/issues/1362",
252+
)
253+
254+
sm.unregisterHandler(capture_id, (IObjectCreatedEvent,))
255+
214256
def test_post_to_folder_with_apostrophe_dont_return_500(self):
215257
response = requests.post(
216258
self.portal.folder1.absolute_url(),

src/plone/restapi/tests/test_content_utils.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
66
from zExceptions import Unauthorized
77
from zope.component import getGlobalSiteManager
8-
from zope.event import notify
9-
from zope.lifecycleevent import ObjectCreatedEvent
108
from zope.lifecycleevent.interfaces import IObjectAddedEvent
119

1210
import unittest
@@ -94,8 +92,7 @@ def move_object(event):
9492

9593
try:
9694
obj = create(self.folder, "Document", "my-document")
97-
notify(ObjectCreatedEvent(obj))
98-
obj = add(self.folder, obj)
95+
obj = add(self.folder, obj, notify_created=True)
9996
self.assertEqual(aq_parent(obj), self.portal)
10097
finally:
10198
sm.unregisterHandler(move_object, (IObjectAddedEvent,))

0 commit comments

Comments
 (0)