Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add items_total to linkintegrity endpoint #1636

Merged
merged 17 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
23 changes: 16 additions & 7 deletions docs/source/endpoints/linkintegrity.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,34 @@ When you create relations between content objects in Plone (for example, via rel
The Plone user interface will use those stored relations to show a warning when you try to delete a content object that is still referenced elsewhere.
Link integrity avoids broken links ("breaches") in the site.

This check includes content objects that are located within a content object ("folderish content").
The `@linkintegrity` endpoint returns the list of reference breaches that would happen if some content items would be deleted.
This information can be used to show the editor a confirmation dialog.

The `@linkintegrity` endpoint returns the list of reference breaches.
If there are none, it will return an empty list (`[]`).
This check includes content objects that are located within a content object ("folderish content").

You can call the `/@linkintegrity` endpoint on the site root with a `GET` request and a list of UIDs in the JSON body:
You can call the `/@linkintegrity` endpoint on the site root with a `GET` request and a list of content UIDs in the JSON body:

```{eval-rst}
.. http:example:: curl httpie python-requests
:request: ../../../src/plone/restapi/tests/http-examples/linkintegrity_get.req
```

The endpoint accepts a single parameter:

`uids`
: A list of object UIDs that you want to check.

The server will respond with the result:

```{literalinclude} ../../../src/plone/restapi/tests/http-examples/linkintegrity_get.resp
:language: http
```

The endpoint accepts a single parameter:
The result includes a list of objects corresponding to the UIDs that were requested.
Each result object includes:

`uids`
: A list of object UIDs that you want to check.
`breaches`
: A list of breaches (sources of relations that would be broken)

`items_total`
: Count of items contained inside the specified UIDs.
1 change: 1 addition & 0 deletions news/1636.breaking
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Change the @linkintegrity endpoint to add `items_total`, the number of contained items which would be deleted. @davisagli, @danalvrz, @pgrunewald
7 changes: 5 additions & 2 deletions src/plone/restapi/services/linkintegrity/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from plone.restapi.interfaces import ISerializeToJsonSummary
from plone.restapi.serializer.converters import json_compatible
from plone.restapi.services import Service
from Products.CMFCore.utils import getToolByName
from zExceptions import BadRequest
from zope.component import getMultiAdapter
from zope.interface import implementer
Expand All @@ -27,13 +28,13 @@ def reply(self):
if not isinstance(uids, list):
uids = [uids]

catalog = getToolByName(self.context, "portal_catalog")
result = []
for uid in uids:
item = uuidToObject(uid)
item_path = "/".join(item.getPhysicalPath())
links_info = item.restrictedTraverse("@@delete_confirmation_info")
breaches = links_info.get_breaches()
if not breaches:
continue
data = getMultiAdapter((item, self.request), ISerializeToJsonSummary)()
data["breaches"] = []
for breach in breaches:
Expand All @@ -43,5 +44,7 @@ def reply(self):
del source["url"]
del source["accessible"]
data["breaches"].append(source)
# subtract one because we don't want to count item_path itself
data["items_total"] = len(catalog(path=item_path)) - 1
result.append(data)
return json_compatible(result)
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
GET /plone/@linkintegrity?uids=SomeUUID000000000000000000000001 HTTP/1.1
GET /plone/@linkintegrity?uids=SomeUUID000000000000000000000002 HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
21 changes: 20 additions & 1 deletion src/plone/restapi/tests/http-examples/linkintegrity_get.resp
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
TODO
HTTP/1.1 200 OK
Content-Type: application/json

[
{
"@id": "http://localhost:55001/plone/doc-2",
"@type": "Document",
"breaches": [
{
"@id": "http://localhost:55001/plone/doc-1",
"title": "First document",
"uid": "SomeUUID000000000000000000000001"
}
],
"description": "",
"items_total": 0,
"review_state": "private",
"title": "Second document"
}
]
28 changes: 28 additions & 0 deletions src/plone/restapi/tests/test_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@
from plone.restapi.tests.statictime import StaticTime
from plone.testing.zope import Browser
from plone.uuid.interfaces import IUUID
from z3c.relationfield import RelationValue
from zope.component import createObject
from zope.component import getMultiAdapter
from zope.component import getUtility
from zope.component.hooks import getSite
from zope.event import notify
from zope.interface import alsoProvides
from zope.intid.interfaces import IIntIds
from zope.lifecycleevent import ObjectModifiedEvent

import collections
import json
Expand Down Expand Up @@ -2713,3 +2717,27 @@ def test_controlpanels_crud_rules(self):
url = "/@controlpanels/content-rules/rule-3"
response = self.api_session.delete(url)
save_request_and_response_for_docs("controlpanels_delete_rule", response)


class TestLinkintegrity(TestDocumentationBase):

layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING

def setUp(self):
super().setUp()

# Create one document with a reference to another
self.doc1 = createContentInContainer(
self.portal, "Document", id="doc-1", title="First document"
)
self.doc2 = createContentInContainer(
self.portal, "Document", id="doc-2", title="Second document"
)
intids = getUtility(IIntIds)
self.doc1.relatedItems = [RelationValue(intids.getId(self.doc2))]
notify(ObjectModifiedEvent(self.doc1))
transaction.commit()

def test_linkintegrity_get(self):
response = self.api_session.get("/@linkintegrity?uids=" + self.doc2.UID())
save_request_and_response_for_docs("linkintegrity_get", response)
30 changes: 26 additions & 4 deletions src/plone/restapi/tests/test_services_linkintegrity.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ def test_required_uids(self):

self.assertEqual(response.status_code, 400)

def test_return_empty_list_for_non_referenced_objects(self):
def test_return_no_breaches_for_non_referenced_objects(self):
response = self.api_session.get(
"/@linkintegrity", params={"uids": [self.doc1.UID()]}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]["breaches"], [])

def test_return_right_breaches_for_reference_field(self):
intids = getUtility(IIntIds)
Expand Down Expand Up @@ -95,8 +96,9 @@ def test_return_right_breaches_for_blocks(self):
response = self.api_session.get(
"/@linkintegrity", params={"uids": [self.doc2.UID()]}
)
breaches = response.json()
self.assertEqual(breaches, [])
result = response.json()
self.assertEqual(len(result), 1)
self.assertEqual(result[0]["breaches"], [])

# create a new content with relations
uid = IUUID(self.doc2)
Expand Down Expand Up @@ -188,3 +190,23 @@ def test_return_breaches_for_contents_in_subfolders(self):
self.assertEqual(len(breaches), 1)
self.assertEqual(breaches[0]["uid"], IUUID(doc_in_folder))
self.assertEqual(breaches[0]["@id"], doc_in_folder.absolute_url())

def test_return_items_total_in_subfolders(self):
# create a folder structure
level1 = createContentInContainer(self.portal, "Folder", id="level1")
createContentInContainer(self.portal["level1"], "Folder", id="level2")
transaction.commit()

# get linkintegrity info for the folder
response = self.api_session.get(
"/@linkintegrity", params={"uids": [level1.UID()]}
)

# we don't expect any links but we still want information
# about how many contained items will be deleted
result = response.json()
self.assertEqual(response.status_code, 200)
self.assertEqual(len(result), 1)
self.assertEqual(result[0]["@id"], level1.absolute_url())
self.assertEqual(result[0]["breaches"], [])
self.assertEqual(result[0]["items_total"], 1)