33.2. REST API endpoints [voting story] – Mastering Plone 6 development – 33. Roundtrip [The voting story] frontend, backend, and REST

REST API endpoints [voting story]

33.2. REST API endpoints [voting story]#

Backend chapter

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 Permissions [voting story].

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.