What's inside
Here’s another part of my tutorial for Django REST framework.
Be sure to catch up with the work we’ve completed in other parts of the series:
In this article, I explore the topic of functional endpoints and API structure nesting.
Note 1: You can find the project code we’re working on in this series in this repository.
In principle, functional endpoints (which in the Django Rest Framework are called actions) perform tasks that don’t fall under CRUD - for example, sending a password reset request.
We’ve been building an application throughout this series that allows managing loaned items. Today, we will build a functional endpoint that sends a reminder to people who borrowed our items.
Actions
Let’s supplement our Friend model with a field which contains the email address:
# models.py
class Friend(OwnedModel):
email = models.EmailField(default='') # remember to add emails using django-admin
# rest of model’s code
and update the database:
$ python manage.py makemigrations && python manage.py migrate
We can now proceed with completing the loan view. We decorate the functional endpoints with the @action decorator. By the way, we can decide whether the action will apply to a single item or their entire list - if so, the path where the endpoint appears will change. We’ll consider both cases below.
Let's consider the single item option first. We want to remind our friend about a specific loan by sending them an email. We want to do that after sending a POST order to the right endpoint. An example implementation looks like this:
from rest_framework.decorators import action
class BorrowedViewset(viewsets.ModelViewSet):
# rest of the code
@action(detail=True, url_path='remind', methods=['post'])
def remind_single(self, request, *args, **kwargs):
obj = self.get_object()
send_mail(
subject=f"Please return my belonging: {obj.what.name}",
message=f'You forgot to return my belonging: "{obj.what.name}" that you borrowed on {obj.when}. Please return it.',
from_email="me@example.com", # your email here
recipient_list=[obj.to_who.email],
fail_silently=False
)
return Response("Email sent.")
The defined endpoint will appear under the path:/api/v1/borrowings/{id}/remind/. Let’s check if the email is sent correctly. If you don’t want to configure the actual sending of emails, use the following configuration that comes in handy during experiments:
# settings.py
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
We can now test our endpoint. In my local environment, the loan with id=1 hasn’t been returned yet. Adjust the example to your specific case:
Our API is secure and we need to send authorized requests. Go back to the second part of this series for more information about that.
$ curl -X POST http://127.0.0.1:8000/api/v1/borrowings/1/remind/ -H
'Authorization: Token fe9a080cf91acb8ed1891e6548f2ace3c66a109f'
The result should be “Email sent.” In the console where you have your server running, you should get a message similar to the following:
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Please return my belonging: Snatch
From: me@example.com
To: friend@example.com
Date: Thu, 25 Apr 2019 08:13:23 -0000
Message-ID: <155618000311.9173.14157612795944796688@mindstorm>
You forgot to return my belonging: "Snatch" that you borrowed on 2018-01-01 12:00:00+00:00. Please return it.
-------------------------------------------------------------------------------
[25/Apr/2019 08:13:23] "POST /api/v1/borrowings/1/remind/ HTTP/1.1" 200 13
The second case would be to send a reminder to all debtors at once. The endpoint should be available under this address: /api/v1/belongings/remind/ and the following decorator should do the trick:
@action(detail=False, url_path='remind', methods=['post'])
def remind_many(self, request, *args, **kwargs):
# ...
Implement this method as a homework assignment. The send_mail function will return 1 if the email has been sent, so you can easily count the number of sent emails and return it in the response.
Nesting
Let's now move to a slightly more advanced topic: nested endpoints. This functionality is not part of the Django Rest Framework, as some guides advise against using it. In my opinion, however, it’s a very convenient way to filter out data.
Several libraries implement data nesting - you can find a list in the DRF documentation. Personally, I like drf-extensions most because it’s a compact package containing many useful things, not just nesting.
In the example below, we'll make a list of all the items borrowed by a specific person. Let’s start with the library installation:
$ pip install drf-extensions
Next, we need to add the appropriate mixin to our ViewSets. That’s how we ensure proper filtering. We add it to all the views that are "on our way":
# views.py
from rest_framework_extensions.mixins import NestedViewSetMixin
# …
class FriendViewset(NestedViewSetMixin, viewsets.ModelViewSet):
# …
# …
class BorrowedViewset(NestedViewSetMixin, viewsets.ModelViewSet):
# …
# …
Next, we need to extend the Router in the file defining the API structure:
# api.py
from rest_framework import routers
from rest_framework_extensions.routers import NestedRouterMixin
from core import views as myapp_views
class NestedDefaultRouter(NestedRouterMixin, routers.DefaultRouter):
pass
router = NestedDefaultRouter()
The modified router allows giving names to specific paths and record nestings in them:
# api.py
router = NestedDefaultRouter()
friends = router.register(r'friends', myapp_views.FriendViewset)
friends.register(
r'borrowings',
myapp_views.BorrowedViewset,
base_name='friend-borrow',
parents_query_lookups=['to_who']
)
As you can see, the syntax is similar to the standard register. At the beginning we have the name of the endpoint and the view that supports it. The parameters base_name and parents_query_lookups are new.
The first determines the base for url names if we’d like to use the reverse() function. The second contains a list of fields relative to the previous models in the queue. The values captured from the url will be juxtaposed with these names and used as a parameter of the filter() method - in our case it will look like this: queryset = Borrowed.objects.filter(to_who={value parsed from url}).
Now we can check which items have been borrowed by one of our friends:
$ curl http://127.0.0.1:8000/api/v1/friends/1/borrowings/
[{"id":1,"what":1,"to_who":1,"when":"2018-01-01T12:00:00Z","returned":null},
{"id":3,"what":1,"to_who":1,"when":"2019-04-17T06:35:22.000236Z","returned":null},
{"id":4,"what":1,"to_who":1,"when":"2019-04-17T06:35:36.546848Z","returned":null}]
There’s one more aspect of nesting worth mentioning: displaying related models. How we retrieve the list of loaned items so that the details of related models are displayed immediately - for example, who borrowed the item (name, email) and what was the loaned item (name).
And that’s going to be the topic of the next part, so stay tuned!