Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/1362.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +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
8 changes: 3 additions & 5 deletions src/plone/restapi/services/content/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@
from zExceptions import Unauthorized
from zope.component import getMultiAdapter
from zope.component import queryMultiAdapter
from zope.event import notify
from zope.interface import alsoProvides
from zope.lifecycleevent import ObjectCreatedEvent

import plone.protect.interfaces

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

if not getattr(deserializer, "notifies_create", False):
notify(ObjectCreatedEvent(obj))

obj = add(self.context, obj, rename=not bool(id_))
obj = add(self.context, obj, rename=not bool(id_), notify_created=True)
else:
obj = add(self.context, obj, rename=not bool(id_))

# Link translation given the translation_of property
if (
Expand Down
7 changes: 3 additions & 4 deletions src/plone/restapi/services/content/tus.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
from uuid import uuid4
from zExceptions import Unauthorized
from zope.component import queryMultiAdapter
from zope.event import notify
from zope.interface import implementer
from zope.lifecycleevent import ObjectCreatedEvent
from zope.publisher.interfaces import IPublishTraverse
from zope.publisher.interfaces import NotFound

Expand Down Expand Up @@ -281,8 +279,9 @@ def create_or_modify_content(self, tus_upload):

if mode == "create":
if not getattr(deserializer, "notifies_create", False):
notify(ObjectCreatedEvent(obj))
obj = add(self.context, obj)
obj = add(self.context, obj, notify_created=True)
else:
obj = add(self.context, obj)

tus_upload.close()
tus_upload.cleanup()
Expand Down
7 changes: 6 additions & 1 deletion src/plone/restapi/services/content/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from zope.container.contained import ObjectAddedEvent
from zope.container.interfaces import INameChooser
from zope.event import notify
from zope.lifecycleevent import ObjectCreatedEvent


def create(container, type_, id_=None, title=None):
Expand Down Expand Up @@ -68,13 +69,15 @@ def create(container, type_, id_=None, title=None):
return obj


def add(container, obj, rename=True):
def add(container, obj, rename=True, notify_created=False):
"""Add an object to a container."""
id_ = getattr(aq_base(obj), "id", None)

# Archetypes objects are already created in a container thus we just fire
# the notification events and rename the object if necessary.
if base_hasattr(obj, "_at_rename_after_creation"):
if notify_created:
notify(ObjectCreatedEvent(obj))
notify(ObjectAddedEvent(obj, container, id_))
notifyContainerModified(container)
if obj._at_rename_after_creation and rename:
Expand All @@ -92,6 +95,8 @@ def add(container, obj, rename=True):
suggestion = obj.Title()
id_ = chooser.chooseName(suggestion, obj)
obj.id = id_
if notify_created:
notify(ObjectCreatedEvent(obj))
new_id = container._setObject(id_, obj)
# _setObject triggers ObjectAddedEvent which can end up triggering a
# content rule to move the item to a different container. In this case
Expand Down
42 changes: 42 additions & 0 deletions src/plone/restapi/tests/test_content_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,48 @@ def record_event(event):
sm.unregisterHandler(record_event, (IObjectAddedEvent,))
sm.unregisterHandler(record_event, (IObjectModifiedEvent,))

def test_post_to_folder_object_created_event_has_final_id(self):
"""IObjectCreatedEvent must fire with the final, INameChooser-resolved ID.

When no 'id' is supplied, plone.restapi assigns a temporary ID and
renames the object via INameChooser before adding it to the container.
Subscribers must see the final ID (e.g. 'my-sequential-doc') on
event.object.id, not the temporary one (e.g. 'document.2026-03-06.1234').

Regression test for https://github.com/plone/plone.restapi/issues/1362
"""
sm = getGlobalSiteManager()
created_event_ids = []

def capture_id(event):
created_event_ids.append(event.object.id)

sm.registerHandler(capture_id, (IObjectCreatedEvent,))

response = requests.post(
self.portal.folder1.absolute_url(),
headers={"Accept": "application/json"},
auth=(SITE_OWNER_NAME, SITE_OWNER_PASSWORD),
json={"@type": "Document", "title": "My Sequential Doc"},
)
self.assertEqual(201, response.status_code)

final_id = response.json()["id"]
self.assertEqual(
1, len(created_event_ids), "Expected exactly one ObjectCreatedEvent"
)

event_id = created_event_ids[0]
self.assertEqual(
final_id,
event_id,
f"IObjectCreatedEvent carried '{event_id}' but final ID was '{final_id}'. "
"Subscribers relying on event.object.id will see a stale value. "
"See https://github.com/plone/plone.restapi/issues/1362",
)

sm.unregisterHandler(capture_id, (IObjectCreatedEvent,))

def test_post_to_folder_with_apostrophe_dont_return_500(self):
response = requests.post(
self.portal.folder1.absolute_url(),
Expand Down
5 changes: 1 addition & 4 deletions src/plone/restapi/tests/test_content_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
from zExceptions import Unauthorized
from zope.component import getGlobalSiteManager
from zope.event import notify
from zope.lifecycleevent import ObjectCreatedEvent
from zope.lifecycleevent.interfaces import IObjectAddedEvent

import unittest
Expand Down Expand Up @@ -94,8 +92,7 @@ def move_object(event):

try:
obj = create(self.folder, "Document", "my-document")
notify(ObjectCreatedEvent(obj))
obj = add(self.folder, obj)
obj = add(self.folder, obj, notify_created=True)
self.assertEqual(aq_parent(obj), self.portal)
finally:
sm.unregisterHandler(move_object, (IObjectAddedEvent,))