Entrée
In this tutorial I'd like to aim to collect some of Django REST filtering concepts and common examples that I use and teach usually. Some of them can be found in official Django REST documentation, however I'm going to walk by a sample repository.
The repo consists as less code as possible which implies it's not designed according to best practices such as api versioning (api/v1
), The Twelve Factor App etc.
NOTE
You can find the code we're working on this tutorial series in this repository: django-rest-filtering-tutorial
You don't need to download and setup the repository in order to follow this series, however I'd like to encorauge to do so, since while you play you learn better as always.
Setup Repo
As mentioned above you don't need to setup the repo however if you like to, —I suggest as well, run below commands:
# Create virtual environment named .venv
$ python -m venv ./.venv
# Activate the virtual environment
$ source ./.venv/bin/activate
# Install python packages
$ python -m pip install -r requirements.txt
# Create db, make migrations and seed database with some dummy data
$ python populate_db.py
# Run development server
$ python manage.py runserver 8000
# or
# ./manage.py runserver 8000
INFO
You don't need to give
8000
port since it's 8default port but I always prefer verbosity.
Also another option is that you can use gnu make and simply run:
$ make start
This make target will run all commands above together so it will setup the repository and run development server.
Sending HTTP Requests
I'm going to use httpie for HTTP requests since it's more human-usable —or let's say more dev-friendly; it provides colorful syntax highlighting (like pretty JSON outputs), persistent sessions, capability of downloading files etc. and it's written in Python but you can use curl as well:
$ http GET http://localhost:8000/authors/
# or with less keystroke, it's same as above
$ http :8000/authors/
# curl equivalent
$ curl http://localhost:8000/authors/
For more info you can look at httpie doc.
Let's start with some basics and send http requests to endpoints.
In order to get all authors, run:
$ http :8000/authors/
HTTP/1.1 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 165
Content-Type: application/json
Date: Fri, 17 Mar 2022 11:03:37 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.8.11
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
[
{
"first_name": "Name1",
"full_name": "Name1 Surname1",
"id": 1,
"last_name": "Surname1",
"user": null
},
{
"first_name": "Name2",
"full_name": "Name2 Surname2",
"id": 2,
"last_name": "Surname2",
"user": null
},
{
"first_name": "Name2",
"full_name": "Name2 Surname2",
"id": 3,
"last_name": "Surname2",
"user": 3
}
]
Mind the slash (/
) suffix after authors; if you don't append it you will get:
$ http :8000/authors
HTTP/1.1 301 Moved Permanently
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Fri, 17 Mar 2022 11:05:35 GMT
Location: /authors/
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.8.11
X-Content-Type-Options: nosniff
You can change this behavior by adding --follow
argument as a flag or as a config in httpie's config file which by default located in ~/.config/httpie/config.json
. As a command flag:
$ http :8000/authors --follow
As a config:
// ~/.config/httpie/config.json
{
"__meta__": {
"about": "HTTPie configuration file",
"help": "https://httpie.org/doc#config",
"httpie": "1.0.3"
},
"default_options": [
"--timeout=300",
"follow"
]
}
Django controls this behavior by APPEND_SLASH setting:
APPEND_SLASH
Default: True
When set to True, if the request URL does not match any of the patterns in the URLconf and it doesn’t end in a slash, an HTTP redirect is issued to the same URL with a slash appended. Note that the redirect may cause any data submitted in a POST request to be lost.
The APPEND_SLASH setting is only used if CommonMiddleware is installed (see Middleware). See also PREPEND_WWW.
Also if you'd like to omit header
part of the response provide --body
flag.
Default Filtering
Default filter for Django REST is no filter, meaning that it returns all objects from the database.
In other words if you try to filter the endpoint with some parameters it will return all objects together just like it did above:
$ http ":8000/authors/?id=1"
[
{
"first_name": "Name1",
"full_name": "Name1 Surname1",
"id": 1,
"last_name": "Surname1",
"user": null
},
{
"first_name": "Name2",
"full_name": "Name2 Surname2",
"id": 2,
"last_name": "Surname2",
"user": null
},
{
"first_name": "Name2",
"full_name": "Name2 Surname2",
"id": 3,
"last_name": "Surname2",
"user": 3
}
]
For better understanding let's look at the responsible get_queryset method from the source code:
# rest_framework/generics.py
def get_queryset(self):
"""
Get the list of items for this view.
This must be an iterable, and may be a queryset.
Defaults to using `self.queryset`.
This method should always be used rather than accessing `self.queryset`
directly, as `self.queryset` gets evaluated only once, and those results
are cached for all subsequent requests.
You may want to override this if you need to provide different
querysets depending on the incoming request.
(Eg. return a list of items that is specific to the user)
"""
assert self.queryset is not None, (
"'%s' should either include a `queryset` attribute, "
"or override the `get_queryset()` method."
% self.__class__.__name__
)
queryset = self.queryset
if isinstance(queryset, QuerySet):
# Ensure queryset is re-evaluated on each request.
queryset = queryset.all()
return queryset
As you can see, it just returns queryset
attribute from the related view which in our case is Article.objects.all()
:
# src/articles/view.py
from rest_framework import viewsets
from src.articles.models import Article
from src.articles.serializers import ArticleSerializer
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
queryset = Article.objects.all()
So as an example if you change queryset
attribute to Article.objects.filter(content__icontains="3")
,
# src/articles/view.py
from rest_framework import viewsets
from src.articles.models import Article
from src.articles.serializers import ArticleSerializer
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
queryset = Article.objects.filter(content__icontains="3")
it only will return Fake Article3 article:
$ http ":8000/authors/"
# or
$ http ":8000/authors/?id=1"
[
{
"author": 3,
"content": "Fake Content3",
"id": 3,
"regions": [],
"title": "Fake Article3"
}
]
At the same time you may realize you can directly change the get_queryset
method and throw the queryset
attribute:
# src/articles/views.py
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
def get_queryset(self):
return Article.objects.filter(content__icontains="3")
Conclusion
That's all for this part of the series. In the next parts you will explore more in depth example and cases.
All done!
Top comments (0)