Content Manipulation#
plone.restapi
does not only expose content objects via a RESTful API.
The API consumer can create, read, update, and delete a content object.
Those operations can be mapped to the HTTP verbs POST
(Create), GET
(Read), PUT
(Update) and DELETE
(Delete).
Manipulating resources across the network using HTTP as an application protocol is one of core principles of the REST architectural pattern. This allows us to interact with a specific resource in a standardized way.
Verb |
URL |
Action |
---|---|---|
|
|
Creates a new document within the folder |
|
|
Request the current state of the document |
|
|
Update the document details |
|
|
Remove the document |
Creating a Resource with POST
#
To create a new resource, we send a POST
request to the resource container.
If we want to create a new document within an existing folder, we send a POST
request to that folder:
POST /plone/folder HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"@type": "Document",
"title": "My Document"
}
curl -i -X POST http://nohost/plone/folder -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"@type": "Document", "title": "My Document"}' --user admin:secret
echo '{
"@type": "Document",
"title": "My Document"
}' | http POST http://nohost/plone/folder Accept:application/json Content-Type:application/json -a admin:secret
requests.post('http://nohost/plone/folder', headers={'Accept': 'application/json', 'Content-Type': 'application/json'}, json={'@type': 'Document', 'title': 'My Document'}, auth=('admin', 'secret'))
By setting the Accept
header, we tell the server that we would like to receive the response in the application/json
representation format.
The Content-Type
header indicates that the body uses the application/json
format.
The request body contains the minimal necessary information needed to create a document (the type and the title).
You could set other properties, such as description
, as well.
A special property during content creation is UID
It requires the user to have the Manage Portal
permission to set it.
Without the permission, the request will fail as Unauthorized
.
Successful Response (201 Created)#
If a resource has been created, the server responds with the 201 Created status code.
The Location
header contains the URL of the newly created resource, and the resource representation is in the payload:
HTTP/1.1 201 Created
Content-Type: application/json
Location: http://localhost:55001/plone/folder/my-document
{
"@components": {
"actions": {
"@id": "http://localhost:55001/plone/folder/my-document/@actions"
},
"aliases": {
"@id": "http://localhost:55001/plone/folder/my-document/@aliases"
},
"breadcrumbs": {
"@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs"
},
"contextnavigation": {
"@id": "http://localhost:55001/plone/folder/my-document/@contextnavigation"
},
"navigation": {
"@id": "http://localhost:55001/plone/folder/my-document/@navigation"
},
"navroot": {
"@id": "http://localhost:55001/plone/folder/my-document/@navroot"
},
"types": {
"@id": "http://localhost:55001/plone/folder/my-document/@types"
},
"workflow": {
"@id": "http://localhost:55001/plone/folder/my-document/@workflow"
}
},
"@id": "http://localhost:55001/plone/folder/my-document",
"@type": "Document",
"UID": "SomeUUID000000000000000000000005",
"allow_discussion": false,
"changeNote": "",
"contributors": [],
"created": "1995-07-31T13:45:00+00:00",
"creators": [
"admin"
],
"description": "",
"effective": null,
"exclude_from_nav": false,
"expires": null,
"id": "my-document",
"is_folderish": false,
"language": "",
"layout": "document_view",
"lock": {
"locked": false,
"stealable": true
},
"modified": "1995-07-31T17:30:00+00:00",
"next_item": {},
"parent": {
"@id": "http://localhost:55001/plone/folder",
"@type": "Folder",
"description": "This is a folder with two documents",
"review_state": "private",
"title": "My Folder",
"type_title": "Folder"
},
"previous_item": {},
"relatedItems": [],
"review_state": "private",
"rights": "",
"subjects": [],
"table_of_contents": null,
"text": null,
"title": "My Document",
"type_title": "Page",
"version": "current",
"versioning_enabled": true,
"working_copy": null,
"working_copy_of": null
}
Unsuccessful Response (400 Bad Request)#
If the resource could not be created, for instance because the title was missing in the request, the server responds with 400 Bad Request:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
'message': 'Required title field is missing'
}
The response body can contain information about why the request failed.
Unsuccessful Response (500 Internal Server Error)#
If the server can not properly process a request, it responds with 500 Internal Server Error:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
'message': 'Internal Server Error'
}
The response body can contain additional information, such as an error trace or a link to the documentation.
Possible POST
Responses#
Possible server responses for a POST
request are:
201 Created (Resource has been created successfully)
400 Bad Request (malformed request to the service)
500 Internal Server Error (server fault, can not recover internally)
POST
Implementation#
A pseudo-code example of the POST
implementation on the server:
try:
order = createOrder()
if order == None:
# Bad Request
response.setStatus(400)
else:
# Created
response.setStatus(201)
except:
# Internal Server Error
response.setStatus(500)
Todo
Link to the real implementation...
Reading a Resource with GET
#
After a successful POST
, we can access the resource by sending a GET
request to the resource URL:
GET /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl -i -X GET http://nohost/plone/folder/my-document -H "Accept: application/json" --user admin:secret
http http://nohost/plone/folder/my-document Accept:application/json -a admin:secret
requests.get('http://nohost/plone/folder/my-document', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
Successful Response (200 OK)#
If a resource has been retrieved successfully, the server responds with 200 OK:
HTTP/1.1 200 OK
Content-Type: application/json
{
"@components": {
"actions": {
"@id": "http://localhost:55001/plone/folder/my-document/@actions"
},
"aliases": {
"@id": "http://localhost:55001/plone/folder/my-document/@aliases"
},
"breadcrumbs": {
"@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs"
},
"contextnavigation": {
"@id": "http://localhost:55001/plone/folder/my-document/@contextnavigation"
},
"navigation": {
"@id": "http://localhost:55001/plone/folder/my-document/@navigation"
},
"navroot": {
"@id": "http://localhost:55001/plone/folder/my-document/@navroot"
},
"types": {
"@id": "http://localhost:55001/plone/folder/my-document/@types"
},
"workflow": {
"@id": "http://localhost:55001/plone/folder/my-document/@workflow"
}
},
"@id": "http://localhost:55001/plone/folder/my-document",
"@type": "Document",
"UID": "SomeUUID000000000000000000000005",
"allow_discussion": false,
"changeNote": "",
"contributors": [],
"created": "1995-07-31T13:45:00+00:00",
"creators": [
"admin"
],
"description": "",
"effective": null,
"exclude_from_nav": false,
"expires": null,
"id": "my-document",
"is_folderish": false,
"language": "",
"layout": "document_view",
"lock": {
"locked": false,
"stealable": true
},
"modified": "1995-07-31T17:30:00+00:00",
"next_item": {},
"parent": {
"@id": "http://localhost:55001/plone/folder",
"@type": "Folder",
"description": "This is a folder with two documents",
"review_state": "private",
"title": "My Folder",
"type_title": "Folder"
},
"previous_item": {},
"relatedItems": [],
"review_state": "private",
"rights": "",
"subjects": [],
"table_of_contents": null,
"text": null,
"title": "My Document",
"type_title": "Page",
"version": "current",
"versioning_enabled": true,
"working_copy": null,
"working_copy_of": null
}
For folderish types, their children are automatically included in the response as items
.
To disable the inclusion, add the GET
parameter include_items=false
to the URL.
By default, only basic metadata is included.
To include additional metadata, you can specify the names of the properties with the metadata_fields
parameter.
See also Retrieving additional metadata.
The following example additionally retrieves the UID
and Creator
:
GET /plone/folder?metadata_fields=UID&metadata_fields=Creator HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl -i -X GET 'http://nohost/plone/folder?metadata_fields=UID&metadata_fields=Creator' -H "Accept: application/json" --user admin:secret
http 'http://nohost/plone/folder?metadata_fields=UID&metadata_fields=Creator' Accept:application/json -a admin:secret
requests.get('http://nohost/plone/folder?metadata_fields=UID&metadata_fields=Creator', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"@components": {
"actions": {
"@id": "http://localhost:55001/plone/folder/@actions"
},
"aliases": {
"@id": "http://localhost:55001/plone/folder/@aliases"
},
"breadcrumbs": {
"@id": "http://localhost:55001/plone/folder/@breadcrumbs"
},
"contextnavigation": {
"@id": "http://localhost:55001/plone/folder/@contextnavigation"
},
"navigation": {
"@id": "http://localhost:55001/plone/folder/@navigation"
},
"navroot": {
"@id": "http://localhost:55001/plone/folder/@navroot"
},
"types": {
"@id": "http://localhost:55001/plone/folder/@types"
},
"workflow": {
"@id": "http://localhost:55001/plone/folder/@workflow"
}
},
"@id": "http://localhost:55001/plone/folder",
"@type": "Folder",
"UID": "SomeUUID000000000000000000000002",
"allow_discussion": false,
"contributors": [],
"created": "1995-07-31T13:45:00+00:00",
"creators": [
"test_user_1_"
],
"description": "This is a folder with two documents",
"effective": null,
"exclude_from_nav": false,
"expires": null,
"id": "folder",
"is_folderish": true,
"items": [
{
"@id": "http://localhost:55001/plone/folder/doc1",
"@type": "Document",
"Creator": "test_user_1_",
"UID": "SomeUUID000000000000000000000003",
"description": "",
"review_state": "private",
"title": "A document within a folder",
"type_title": "Page"
},
{
"@id": "http://localhost:55001/plone/folder/doc2",
"@type": "Document",
"Creator": "test_user_1_",
"UID": "SomeUUID000000000000000000000004",
"description": "",
"review_state": "private",
"title": "A document within a folder",
"type_title": "Page"
},
{
"@id": "http://localhost:55001/plone/folder/my-document",
"@type": "Document",
"Creator": "admin",
"UID": "SomeUUID000000000000000000000005",
"description": "",
"review_state": "private",
"title": "My Document",
"type_title": "Page"
}
],
"items_total": 3,
"language": "",
"layout": "listing_view",
"lock": {},
"modified": "1995-07-31T17:30:00+00:00",
"nextPreviousEnabled": false,
"next_item": {},
"parent": {
"@id": "http://localhost:55001/plone",
"@type": "Plone Site",
"description": "",
"title": "Plone site",
"type_title": "Plone Site"
},
"previous_item": {},
"relatedItems": [],
"review_state": "private",
"rights": "",
"subjects": [],
"title": "My Folder",
"type_title": "Folder",
"version": "current",
"working_copy": null,
"working_copy_of": null
}
Note
For folderish types, collections or search results, the results will be batched if the size of the resultset exceeds the batch size. See Batching for more details on how to work with batched results.
Unsuccessful response (404 Not Found)#
If a resource cannot be found, the server will respond with 404 Not Found:
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"message": "Resource not found: https://<resource url>",
"type": "NotFound"
}
GET
Implementation#
A pseudo-code example of the GET
implementation on the server:
try:
order = getOrder()
if order == None:
# Not Found
response.setStatus(404)
else:
# OK
response.setStatus(200)
except:
# Internal Server Error
response.setStatus(500)
You can find implementation details in the plone.restapi.services.content.add.FolderPost class.
GET
Responses#
Possible server responses for a GET
request are:
Updating a Resource with PATCH
#
To update an existing resource, we send a PATCH
request to the server.
PATCH
allows providing just a subset of the resource, such as the values you actually want to change.
If you send the value null
for a field, the field's content will be deleted, and the missing_value
defined for the field in the schema will be set.
Note that this is not possible if the field is required
:
PATCH /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"title": "My New Document Title"
}
curl -i -X PATCH http://nohost/plone/folder/my-document -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"title": "My New Document Title"}' --user admin:secret
echo '{
"title": "My New Document Title"
}' | http PATCH http://nohost/plone/folder/my-document Accept:application/json Content-Type:application/json -a admin:secret
requests.patch('http://nohost/plone/folder/my-document', headers={'Accept': 'application/json', 'Content-Type': 'application/json'}, json={'title': 'My New Document Title'}, auth=('admin', 'secret'))
Successful Response (204 No Content)#
A successful response to a PATCH
request will be indicated by a 204 No Content response by default:
HTTP/1.1 204 No Content
Successful Response (200 OK)#
You can get the object representation by adding a Prefer
header with a value of return=representation
to the PATCH
request.
In this case, the response will be a 200 OK:
PATCH /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Prefer: return=representation
Content-Type: application/json
{
"title": "My New Document Title"
}
curl -i -X PATCH http://nohost/plone/folder/my-document -H "Accept: application/json" -H "Content-Type: application/json" -H "Prefer: return=representation" --data-raw '{"title": "My New Document Title"}' --user admin:secret
echo '{
"title": "My New Document Title"
}' | http PATCH http://nohost/plone/folder/my-document Accept:application/json Content-Type:application/json Prefer:return=representation -a admin:secret
requests.patch('http://nohost/plone/folder/my-document', headers={'Accept': 'application/json', 'Content-Type': 'application/json', 'Prefer': 'return=representation'}, json={'title': 'My New Document Title'}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json
{
"@components": {
"actions": {
"@id": "http://localhost:55001/plone/folder/my-document/@actions"
},
"aliases": {
"@id": "http://localhost:55001/plone/folder/my-document/@aliases"
},
"breadcrumbs": {
"@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs"
},
"contextnavigation": {
"@id": "http://localhost:55001/plone/folder/my-document/@contextnavigation"
},
"navigation": {
"@id": "http://localhost:55001/plone/folder/my-document/@navigation"
},
"navroot": {
"@id": "http://localhost:55001/plone/folder/my-document/@navroot"
},
"types": {
"@id": "http://localhost:55001/plone/folder/my-document/@types"
},
"workflow": {
"@id": "http://localhost:55001/plone/folder/my-document/@workflow"
}
},
"@id": "http://localhost:55001/plone/folder/my-document",
"@type": "Document",
"UID": "SomeUUID000000000000000000000005",
"allow_discussion": false,
"changeNote": "",
"contributors": [],
"created": "1995-07-31T13:45:00+00:00",
"creators": [
"admin"
],
"description": "",
"effective": null,
"exclude_from_nav": false,
"expires": null,
"id": "my-document",
"is_folderish": false,
"language": "",
"layout": "document_view",
"lock": {
"locked": false,
"stealable": true
},
"modified": "1995-07-31T17:30:00+00:00",
"next_item": {},
"parent": {
"@id": "http://localhost:55001/plone/folder",
"@type": "Folder",
"description": "This is a folder with two documents",
"review_state": "private",
"title": "My Folder",
"type_title": "Folder"
},
"previous_item": {},
"relatedItems": [],
"review_state": "private",
"rights": "",
"subjects": [],
"table_of_contents": null,
"text": null,
"title": "My New Document Title",
"type_title": "Page",
"version": "current",
"versioning_enabled": true,
"working_copy": null,
"working_copy_of": null
}
See the full specifications in RFC 5789: PATCH
Method for HTTP.
Replacing a Resource with PUT
#
Note
PUT
is not implemented yet.
To replace an existing resource, we send a PUT
request to the server:
Todo
Add example.
In accordance with the HTTP specification, a successful PUT
will not create a new resource or produce a new URL.
PUT
expects the entire resource representation to be supplied to the server, rather than just changes to the resource state.
This is usually not a problem since the consumer application requested the resource representation before a PUT
anyways.
When the PUT
request is accepted and processed by the service, the consumer will receive a 204 No Content response (200 OK would be a valid alternative).
Successful Update (204 No Content)#
When a resource has been updated successfully, the server sends a 204 No Content response:
Todo
Add example.
Unsuccessful Update (409 Conflict)#
Sometimes requests fail due to incompatible changes. The response body includes additional information about the problem:
Todo
Add example.
PUT
Implementation#
A pseudo-code example of the PUT
implementation on the server:
try:
order = getOrder()
if order:
try:
saveOrder()
except conflict:
response.setStatus(409)
# OK
response.setStatus(200)
else:
# Not Found
response.setStatus(404)
except:
# Internal Server Error
response.setStatus(500)
Todo
Link to the real implementation...
PUT
Responses#
Possible server responses for a PUT
request are:
POST
vs. PUT
#
Using POST
or PUT
depend on the desired outcome.
Use
POST
to create a resource identified by a service-generated URI.Use
POST
to append a resource to a collection identified by a service-generated URI.Use
PUT
to overwrite a resource.
This follows RFC 7231: HTTP 1.1: PUT
Method.
Removing a Resource with DELETE
#
We can delete an existing resource by sending a DELETE
request:
DELETE /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
curl -i -X DELETE http://nohost/plone/folder/my-document -H "Accept: application/json" --user admin:secret
http DELETE http://nohost/plone/folder/my-document Accept:application/json -a admin:secret
requests.delete('http://nohost/plone/folder/my-document', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
A successful response will be indicated by a 204 No Content response:
HTTP/1.1 204 No Content
DELETE
Implementation#
A pseudo-code example of the DELETE
implementation on the server:
try:
order = getOrder()
if order:
if can_delete(order):
# No Content
response.setStatus(204)
else:
# Not Allowed
response.setStatus(405)
else:
# Not Found
response.setStatus(404)
except:
# Internal Server Error
response.setStatus(500)
Todo
Link to the real implementation...
DELETE
Responses#
Possible responses to a DELETE
request are:
404 Not Found (if the resource does not exist)
405 Method Not Allowed (if deleting the resource is not allowed)
Reordering sub resources#
The resources contained within a resource can be reordered using the ordering
key with a PATCH
request on the container.
Use the obj_id
subkey to specify which resource to reorder.
The subkey delta
can be top
, bottom
, or a negative or positive integer for moving up or down.
Reordering resources within a subset of resources can be done using the subset_ids
subkey.
A response of 400 BadRequest
with a message Client/server ordering mismatch
will be returned if the value is not in the same order as server side.
A response of 400 BadRequest
with a message Content ordering is not supported by this resource
will be returned if the container does not support ordering:
PATCH /plone/folder/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"ordering": {"obj_id": "item_3", "delta": "top", "subset_ids": ["item_1", "item_3", "item5"]}
}
curl -i -X PATCH http://nohost/plone/folder/my-document -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"ordering": {"delta": "top", "obj_id": "item_3", "subset_ids": ["item_1", "item_3", "item5"]}}' --user admin:secret
echo '{
"ordering": {
"delta": "top",
"obj_id": "item_3",
"subset_ids": [
"item_1",
"item_3",
"item5"
]
}
}' | http PATCH http://nohost/plone/folder/my-document Accept:application/json Content-Type:application/json -a admin:secret
requests.patch('http://nohost/plone/folder/my-document', headers={'Accept': 'application/json', 'Content-Type': 'application/json'}, json={'ordering': {'delta': 'top', 'obj_id': 'item_3', 'subset_ids': ['item_1', 'item_3', 'item5']}}, auth=('admin', 'secret'))
To rearrange all items in a folderish context, use the sort
key.
The on
subkey defines the catalog index to be sorted on.
The order
subkey indicates either the ascending
or descending
order of items.
A response 400 BadRequest
with a message Content ordering is not supported by this resource
will be returned if the container does not support ordering:
PATCH /plone/folder HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"sort": {"on": "modified", "order": "descending"}
}
curl -i -X PATCH http://nohost/plone/folder -H "Accept: application/json" -H "Content-Type: application/json" --data-raw '{"sort": {"on": "modified", "order": "descending"}}' --user admin:secret
echo '{
"sort": {
"on": "modified",
"order": "descending"
}
}' | http PATCH http://nohost/plone/folder Accept:application/json Content-Type:application/json -a admin:secret
requests.patch('http://nohost/plone/folder', headers={'Accept': 'application/json', 'Content-Type': 'application/json'}, json={'sort': {'on': 'modified', 'order': 'descending'}}, auth=('admin', 'secret'))