Skip to content

Commit

Permalink
Add universal relations service (#1602)
Browse files Browse the repository at this point in the history
* Query relations with universal endpoint.

* Catch Products.CMFPlone.relationhelper ImportError

* Remove unused imports

* Add option to restrict number of queried relations.

* Rebuild relations

* @relations: onlyBroken: Return broken relations

* Update relations.md

* Add tests for relations

* Draft/placeholder: provide vocabulary query of relation

* Provide vocabulary query of relation for …

fields with registered named StaticCatalogVocabularyQuery

* Add relations

* Modification of relations only if plone.api.relation available

* Delete relations.

Now some more tests and it's done!

* flake

* Create 1432.feature

* Add UID to relationvalue_converter summary

Support RelationList field with named StaticCatalogVocabulary and SelectWidget

* Update resps

* Fix exceptions

* Update resps

* Restore make_summary

* Update tests

* More tests

* Delete relations by relation name, source, or target, or combination

* Remove debug logging

* Fix f-strings

* Fix error messages

* Update test_relations_get.py: test if relation stuff available

* Update test_relations_get.py

* Delete test_relations_get.py

* Test: Is vocabulary query included in response

Register named StaticCatalogVocabulary

* Moooree tests

* How to use @relations service to query or modify relations

* Update relations.md

* broken link

* Delete by relation name

* more tests for deleting relations

* Test for anonymous

* Use api.content.get with authorization check included

* Revert "Use api.content.get with authorization check included"

No plone.api, except in tests!

* make flake shake

* Update docu

* prettier

* Provide querying by source and target (SearchableText or path)

Assures relations control panel can handle many relations

* Fix required permissions in documentation

* Log errors on creating relations

* Log which source or target not found

* myst fix

* Update upgrade-guide.md

* Apply suggestions from code review

Co-authored-by: Steve Piercy <web@stevepiercy.com>

* Apply suggestions from code review

Co-authored-by: David Glick <david@glicksoftware.com>
Co-authored-by: Steve Piercy <web@stevepiercy.com>

* Log source, target if not found. Log and reply with requested source, target, relation

* Do not remove image info from summary of relation source and target on querying

* Run black

* Enhance response on failure on creating or deleting relation

* Fix error type

Co-authored-by: David Glick <david@glicksoftware.com>

* Amend documentation of querying options

* Deleting relations: Rely on plone.api

* Fix documentation

* Update delete.py

* Apply suggestions from code review

Co-authored-by: Steve Piercy <web@stevepiercy.com>

* Apply suggestions from code review

Co-authored-by: Steve Piercy <web@stevepiercy.com>

* Update response code and content on deleting relations

* Update format of @relations GET

* Request to rebuild relations now POST with /rebuild

* run black

* black

* Update docs description as suggested by Steve

* Update response format in onlyBroken docs, and remove old unused example

* Change relations -> stats in the get_stats response

* Fix tests according to 'Change relations -> stats in the get_stats response'

* Uncomment "delete by relation name" after release of plone.api 2.0.2

* Respond with "Unprocessable Entity" if creating fails

* docu more precise

* run black

* Apply suggestions from code review

Co-authored-by: Steve Piercy <web@stevepiercy.com>

* Apply suggestions from code review

Co-authored-by: Steve Piercy <web@stevepiercy.com>

* Precise documentation on deleting list of relations

A list of relations can be deleted in one request. Maybe the wording is not optimal 'list of single relations'

* run black

* Fix format for broken relations according format for relations

* run black

* Apply docs suggestions from code review

Co-authored-by: Steve Piercy <web@stevepiercy.com>

---------

Co-authored-by: Alin Voinea <contact@avoinea.com>
Co-authored-by: Timo Stollenwerk <tisto@users.noreply.github.com>
Co-authored-by: Steve Piercy <web@stevepiercy.com>
Co-authored-by: David Glick <david@glicksoftware.com>
Co-authored-by: Érico Andrei <ericof@gmail.com>
  • Loading branch information
6 people committed May 23, 2023
1 parent d3daf77 commit 880e983
Show file tree
Hide file tree
Showing 54 changed files with 2,137 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/source/endpoints/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ principals
querystring
querystringsearch
registry
relations
roles
searching
system
Expand Down
314 changes: 314 additions & 0 deletions docs/source/endpoints/relations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
---
myst:
html_meta:
"description": "Create, query, and delete relations between content items with the relations endpoint."
"property=og:description": "Create, query, and delete relations between content items with the relations endpoint."
"property=og:title": "Relations"
"keywords": "Plone, plone.restapi, REST, API, relations, service, endpoint"
---

(restapi-relations-label)=

# Relations

Plone's relations represent binary relationships between content objects.

A single relation is defined by source, target, and relation name.

You can define relations either with content type schema fields `RelationChoice` or `RelationList`, or with types `isReferencing` or `iterate-working-copy`.

- Relations based on fields of a content type schema are editable by users.
- The relations `isReferencing` (block text links to a Plone content object) and `iterate-working-copy` (working copy is enabled and the content object is a working copy) are not editable.
They are created and deleted with links in text, respectively creating and deleting working copies.

You can create, query, and delete relations by interacting through the `@relations` endpoint on the site root.
Querying relations with the `@relations` endpoint requires the `zope2.View` permission on both the source and target objects.
Therefore results include relations if and only if both the source and target are accessible by the querying user.
Creating and deleting relations requires `zope2.View` permission on the target object and `cmf.ModifyPortalContent` permission on the source object.

(restapi-relations-getting-statistics-for-all-relations-label)=

## Getting statistics for all relations

The call without any parameters returns statistics on all existing relations to which the user has access.

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

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

(restapi-relations-querying-relations-label)=

## Querying relations

You can query relations by a single source, target, or relation type.
Combinations are allowed.
The source and target must be either a UID or path.

Queried relations require the `View` permission on the source and target.
If the user lacks permission to access these relations, then they are omitted from the query results.

The relations are grouped by relation name, source, and target, and are provided in a summarized format.


Query relations of a **relation type**:

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

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

Query relations of a **source** object by path:

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

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

Query relations of a **source** object by UID:

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

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

Query relations by **relation name and source**:

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

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

Query relations to a **target**:

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

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

### Refining

Querying can be further refined by applying the `query_target` search parameter to restrict the source or target to either contain a search string or be located under a path.

Search string example:

```
/@relations?relation=comprisesComponentPart&query_target=wheel
```

Path example:

```
/@relations?relation=comprisesComponentPart&query_target=/inside/garden
```

### Limit the results

Limit the number of results by `max` to, for example, at most 100 results:

```
/@relations?relation=comprisesComponentPart&source=/documents/doc-1&max=100
```

### Only broken relations

Retrieve items with broken relations by querying with `onlyBroken`:

```
/@relations?onlyBroken=true
```

This returns a JSON object, for example:

```json
{
"@id": "http://localhost:55001/plone/@relations?onlyBroken=true",
"relations": {
"relatedItems": {
"items": [
"http://localhost:55001/plone/document-2",
],
"items_total": 1
}
}
}
```


(restapi-relations-creating-relations-label)=

## Creating relations

You can create relations by providing a list of the source, target, and name of the relation.
The source and target must be either a UID or path.

If the relation is based on a `RelationChoice` or `RelationList` field of the source object, the value of the field is updated accordingly.

Create a relation by **path**:

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

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

Create a relation by **UID**:

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

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

If either the source or target do not exist, then an attempt to create a relation will fail, and will return a `422 Unprocessable Entity` response.

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

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

(restapi-relations-deleting-relations-label)=

## Deleting relations

You can delete relations by relation name, source object, target object, or a combination of these.
You can also delete relations by providing a list of relations.

If a deleted relation is based on a `RelationChoice` or `RelationList` field, the value of the field is removed or updated accordingly on the source object.

### Delete a list of relations

You can delete relations by either UID or path.

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

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

If either the source or target do not exist, then an attempt to delete a relation will fail, and will return a `422 Unprocessable Entity` response.

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

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

### Delete relations by relation name

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

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

### Delete relations by source

You can delete relations by either source UID or path.

The following example shows how to delete a relation by source path.

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

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

### Delete relations by target

You can delete relations by either target UID or path.

The following example shows how to delete a relation by target path.

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

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

### Delete relations by a combination of source, target, and relation name

You can delete relations by a combination of either any two of their relation name, source, and target, or a combination of all three.
In the following example, you would delete a relation by its relation name and target.

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

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


## Fix relations

Broken relations can be fixed by releasing and re-indexing them.
A successfully fixed relation will return a `204 No Content` response.

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

In rare cases, you may need to flush the `intIds`.
You can rebuild relations by flushing the `intIds` with the following HTTP POST request.

```{warning}
If your code relies on `intIds`, you should take caution and think carefully before you flush them.
```


```{eval-rst}
.. http:example:: curl httpie python-requests
:request: ../../../src/plone/restapi/tests/http-examples/relations_rebuild_with_flush.req
```
1 change: 1 addition & 0 deletions news/1432.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Create relations service. Query, add, delete. @ksuess
1 change: 1 addition & 0 deletions src/plone/restapi/services/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<include package=".querystring" />
<include package=".querystringsearch" />
<include package=".registry" />
<include package=".relations" />
<include package=".roles" />
<include package=".rules" />
<include package=".search" />
Expand Down
36 changes: 36 additions & 0 deletions src/plone/restapi/services/relations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
try:
from plone.api.relation import create as api_relation_create
from plone.api.relation import delete as api_relation_delete
except ImportError:
api_relation_create = None
api_relation_delete = None

from plone.app.uuid.utils import uuidToObject
from Products.CMFCore.DynamicType import DynamicType
from zope.component.hooks import getSite


def plone_api_content_get(path=None, UID=None):
"""Get an object.
copy pasted from plone.api
"""
if path:
site = getSite()
site_absolute_path = "/".join(site.getPhysicalPath())
if not path.startswith("{path}".format(path=site_absolute_path)):
path = "{site_path}{relative_path}".format(
site_path=site_absolute_path,
relative_path=path,
)
try:
content = site.restrictedTraverse(path)
except (KeyError, AttributeError):
return None # When no object is found don't raise an error
else:
# Only return a content if it implements DynamicType,
# which is true for Dexterity content and Comment (plone.app.discussion)
return content if isinstance(content, DynamicType) else None

elif UID:
return uuidToObject(UID)

0 comments on commit 880e983

Please sign in to comment.