32.2. REST API endpoints [voting story]#
Get the code! training.votable
To be solved task in this part:
Provide access to voting for the Volto frontend
In this part you will:
Register and write a custom endpoint
Topics covered:
Extending plone.restapi
Services and Endpoints
Out of the box Volto has no access to the logic of the voting behavior of the last chapter.
We need to create a REST API endpoint that can be addressed by GET, POST and DELETE requests.
The adapter training.votable.behaviors.votable.Votable
has the logic needed for voting.
The key features are votes
to get the current votes, vote
to actively cast a vote and clear
to clear existing votes.
In src/training/votable/
create a folder structure like the following:
api/
├── __init__.py
├── configure.zcml
├── voting.py
We include the new module api
in the packages' main configure.zcml
:
1<include package=".browser" />
2<include package=".api" />
The services for the endpoint @votes
are now to be implemented in voting.py
.
1# -*- coding: utf-8 -*-
2from plone import api
3from plone.protect.interfaces import IDisableCSRFProtection
4from plone.restapi.deserializer import json_body
5from plone.restapi.services import Service
6from zExceptions import Unauthorized
7from zope.globalrequest import getRequest
8from zope.interface import alsoProvides
9
10from training.votable import (
11 CanVotePermission,
12 ClearVotesPermission,
13 ViewVotesPermission,
14)
15from training.votable.behaviors.votable import IVotable
16
17
18class VotingGet(Service):
19 """Voting information about the current object"""
20
21 def reply(self):
22 can_view_votes = api.user.has_permission(ViewVotesPermission, obj=self.context)
23 if not can_view_votes:
24 raise Unauthorized("User not authorized to view votes.")
25 return vote_info(self.context, self.request)
26
27
28class VotingPost(Service):
29 """Vote for an object"""
30
31 def reply(self):
32 alsoProvides(self.request, IDisableCSRFProtection)
33 can_vote = api.user.has_permission(CanVotePermission, obj=self.context)
34 if not can_vote:
35 raise Unauthorized("User not authorized to vote.")
36 voting = IVotable(self.context)
37 data = json_body(self.request)
38 vote = data["rating"]
39 voting.vote(vote, self.request)
40
41 return vote_info(self.context, self.request)
42
43
44class VotingDelete(Service):
45 """Unlock an object"""
46
47 def reply(self):
48 alsoProvides(self.request, IDisableCSRFProtection)
49 can_clear_votes = api.user.has_permission(
50 ClearVotesPermission, obj=self.context
51 )
52 if not can_clear_votes:
53 raise Unauthorized("User not authorized to clear votes.")
54 voting = IVotable(self.context)
55 voting.clear()
56 return vote_info(self.context, self.request)
57
58
59def vote_info(obj, request=None):
60 """Returns voting information about the given object."""
61 if not request:
62 request = getRequest()
63 voting = IVotable(obj)
64 info = {
65 "average_vote": voting.average_vote(),
66 "total_votes": voting.total_votes(),
67 "has_votes": voting.has_votes(),
68 "already_voted": voting.already_voted(request),
69 "can_vote": api.user.has_permission(CanVotePermission, obj=obj),
70 "can_clear_votes": api.user.has_permission(ClearVotesPermission, obj=obj),
71 }
72 return info
The GET service is highlighted.
If we look at the code, we see that the service inherits necessary properties from plone.restapi.services.Service
by subclassing.
The reply
method implements what should be returned on a GET request to endpoint @votes
.
It checks the permission to vote
It accesses the behavior logic to return the votes.
How can the service use the features we implemented with the behavior?
We will register the services for the behaviors' marker interface.
With that an instance of a content type that has the behavior enabled, can be adapted by IVotable(context)
.
We skip the permissions and talk about this in a later chapter reusable.
With a registration in configure.zcml
the endpoint is addressable.
1<configure
2 xmlns="http://namespaces.zope.org/zope"
3 xmlns:browser="http://namespaces.zope.org/browser"
4 xmlns:plone="http://namespaces.plone.org/plone"
5 i18n_domain="training.votable">
6
7 <plone:service
8 method="GET"
9 for="training.votable.behaviors.votable.IVotableMarker"
10 factory=".voting.VotingGet"
11 name="@votes"
12 permission="zope2.View"
13 />
14
15 <plone:service
16 method="POST"
17 for="training.votable.behaviors.votable.IVotableMarker"
18 factory=".voting.VotingPost"
19 name="@votes"
20 permission="training.votable.can_vote"
21 />
22
23 <plone:service
24 method="DELETE"
25 for="training.votable.behaviors.votable.IVotableMarker"
26 factory=".voting.VotingDelete"
27 name="@votes"
28 permission="zope2.ViewManagementScreens"
29 />
30
31</configure>
Note that all have the same name @votes
but will provide different functionality depending on the method of the request (GET, POST, DELETE).
This is not required but a convention many endpoints follow.
We could also name them more in sync with their functionality.
In our example the permission checks are delegated to the services themselves and we use zope2.View
as permission.
The services are all only available on content that provides the behaviors' marker interface training.votable.behaviors.votable.IVotableMarker
that we declared in the last chapter.
If you have postman
installed, you can address the new endpoint for testing purpose.
Be sure to authenticate and add a header to accept JSON
.