Skip to content

Commit

Permalink
Add items_total to linkintegrity endpoint (#1636)
Browse files Browse the repository at this point in the history
* Add items_total to linkintegrity endpoint

* changelog

* Add missing documentation for linkintegrity endpoint

* Update docs/source/endpoints/linkintegrity.md

* Run tests with Python 3.9

* Add upgrade guide for link integrity

---------

Co-authored-by: Timo Stollenwerk <tisto@users.noreply.github.com>
Co-authored-by: Timo Stollenwerk <stollenwerk@kitconcept.com>
  • Loading branch information
3 people authored Aug 25, 2023
1 parent f5ec2f5 commit 1c08b5c
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 16 deletions.
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.
25 changes: 25 additions & 0 deletions docs/source/upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,31 @@ myst:
This upgrade guide lists all breaking changes in `plone.restapi`.
It explains the steps that are needed to upgrade to the latest version.

## Upgrading to `plone.restapi` 9.x

### Link Integrity

When calling the @linkintegrity endpoint in `plone.restapi` before 9.0.0, a content object with no link integrity breaches would return just an empty list in the response body:

`[]`

In `plone.restapi` 9.0.0, the following response would be returned with a `breaches` attribute with an empty list:

```
[
{
"@id": "http://localhost:55001/plone/doc-2",
"@type": "Document",
"breaches": [],
"description": "",
"items_total": 0,
"review_state": "private",
"title": "Second document",
"type_title": "Page"
}
]
```

## Upgrading to `plone.restapi` 8.x

`plone.restapi` 8.x dropped support for Python 2 and Plone 5.1 and 4.3.
Expand Down
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
22 changes: 21 additions & 1 deletion src/plone/restapi/tests/http-examples/linkintegrity_get.resp
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
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",
"type_title": "Page"
}
]
30 changes: 29 additions & 1 deletion src/plone/restapi/tests/test_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@
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
from plone.app.testing import popGlobalRegistry
from plone.app.testing import pushGlobalRegistry
from plone.restapi.testing import register_static_uuid_utility
from zope.component.hooks import getSite

import collections
import json
Expand Down Expand Up @@ -2853,3 +2857,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)

0 comments on commit 1c08b5c

Please sign in to comment.