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.

httpie-demo

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ~/.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:

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():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 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"),

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 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:

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!