Entrée

We saw in the previous part — django rest filtering tutorial part 3, how to filter against the URL.

$ http :8000/authors/Surname3
# or with curl
# $ curl http://localhost:8000/authors/Surname3

[
    {
        "first_name": "Name3",
        "full_name": "Name3 Surname3",
        "id": 3,
        "last_name": "Surname3",
        "user": 3
    }
]

In this post we’ll see how to filter with query params, like:

# httpie
$ http ":8000/authors/?user=Surname3"
# or with curl
# $ curl http://localhost:8000/authors/?user=Surname3

HTTP/1.1 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 92
Content-Type: application/json
Date: Sat, 14 Apr 2022 10:38:27 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": "Name3",
        "full_name": "Name3 Surname3",
        "id": 3,
        "last_name": "Surname3",
        "user": 3
    }
]

Info

You can find usage details of httpie in the first post of this series:

The Code

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.

User ID

In this section, as an example we’ll filter authors with it’s users' id/pk.

# httpie
$ http ":8000/authors/?user=3"
# or with curl
# $ curl http://localhost:8000/authors/?user=3

[
    {
        "first_name": "Name3",
        "full_name": "Name3 Surname3",
        "id": 3,
        "last_name": "Surname3",
        "user": 3
    }
]

In order to filter with params, we’re going to override get_queryset method of our view:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from typing import Optional

from rest_framework import viewsets

from src.authors.models import Author
from src.authors.serializers import AuthorSerializer


class AuthorViewSet(viewsets.ModelViewSet):
    serializer_class = AuthorSerializer
    queryset = Author.objects.all()

    def get_queryset(self):
        user: Optional[int] = self.request.query_params.get("user", None)
        if user is not None:
            return Author.objects.filter(user=user)

        return super().get_queryset()

As we can see, our user param lies in the query_params attribute of the request object. For more insight, below you can find the related code from Django Rest Framework’s Github:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# rest_framework/request.py
...

class Request:
    """
    Wrapper allowing to enhance a standard `HttpRequest` instance.

    Kwargs:
        - request(HttpRequest). The original request instance.
        - parsers(list/tuple). The parsers to use for parsing the
          request content.
        - authenticators(list/tuple). The authenticators used to try
          authenticating the request's user.
    """

    ...

    @property
    def query_params(self):
        """
        More semantically correct name for request.GET.
        """
        return self._request.GET

    ...

And here is the permalink to it: drf request

You see that it’s just a wrapper around request.GET and when you examine it’s representation you recognise that it’s a QueryDict object:

>>> from django.http import QueryDict
>>> isinstance(self.request.query_params, QueryDict)

>>> self.request.query_params
>>> <QueryDict: {'user': ['3']}>

Info

Django Doc - QueryDict

class QueryDict

In an HttpRequest object, the GET and POST attributes are instances of django.http.QueryDict, a dictionary-like class customized to deal with multiple values for the same key. This is necessary because some HTML form elements, notably , pass multiple values for the same key.

The QueryDicts at request.POST and request.GET will be immutable when accessed in a normal request/response cycle. To get a mutable version you need to use QueryDict.copy().

URLs - Router

Our related urls/router to AuthorViewSet looks like below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# authors/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from src.authors import views

# Create a router and register our viewsets with it
router = DefaultRouter()
router.register(r"authors", views.AuthorViewSet)

urlpatterns = router.urls

User and Author model

If you want to see the User and Author model, you can look at the repo, however for easy access I put them below as well. In order to examine them just expand the below section.

User and Author model

User model:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# src/users/models.py
from django.contrib.auth.models import AbstractUser, UserManager
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _


class User(AbstractUser):
    """Default custom user"""

    #: First and last name do not cover name patterns around the globe
    name = models.CharField(_("Name of User"), blank=True, max_length=255)
    first_name = None  # type: ignore
    last_name = None  # type: ignore

    objects = UserManager()

Author model:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# src/authors/models.py
from django.db import models
from django.conf import settings

USER = settings.AUTH_USER_MODEL


class Author(models.Model):
    """
    Author entity

    Provides first_name and last_name, since he/she can write under a Pen Name
    """
    user = models.ForeignKey(to=USER, on_delete=models.SET_NULL, blank=True, null=True)
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)

    def __str__(self):
        return f"{self.first_name} {self.last_name}"

    @property
    def full_name(self):
        return f"{self.first_name} {self.last_name}"

Any Name

As we did in our previous post, let’s filter authors with any name;

  • first_name of author,
  • last_name of author,
  • name of user,
  • username of user

Modify our get_queryset to get the desired filtering behavior:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from rest_framework import viewsets
from django.db.models import Q

from src.authors.models import Author
from src.authors.serializers import AuthorSerializer


class AuthorViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = AuthorSerializer
    queryset = Author.objects.all()

    def get_queryset(self):
        if (name := self.request.get_queryset.get("name", None)) is not None:
            return Author.objects.filter(
                Q(first_name__icontains=name)
                | Q(last_name__icontains=name)
                | Q(user__name__icontains=name)
                | Q(user__username__icontains=name)
            )

        return super().get_queryset()

For this complex filtering we take advantage of Q object with | (OR) logical operator.

One improvement that we can add is that we can check whether given param is in our desired param list:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from django.db.models import Q
from rest_framework import viewsets

from src.authors.models import Author
from src.authors.serializers import AuthorSerializer


class AuthorViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = AuthorSerializer
    queryset = Author.objects.all()

    def get_queryset(self):
        qp = self.request.query_params

        param_list = ("first_name", "last_name", "name", "username")
        is_desired_param = any([param in qp for param in param_list])
        if len(qp) > 0 and not is_desired_param:
            return Author.objects.none()

        if (name := qp.get("name", None)) is not None:
            return Author.objects.filter(
                Q(first_name__icontains=name)
                | Q(last_name__icontains=name)
                | Q(user__name__icontains=name)
                | Q(user__username__icontains=name)
            )

        return super().get_queryset()

With this guard clause we check given paramater is in param_list, if not, we return an empty list [], with Author.objects.none().

If you have multiple or more complex filter query paramaters, you should consider to implement Third Party Packages like django-filter which I’ll implement and explain in a later post of this series.

Conclusion

In this tutorial we learned how to filter with query paramaters with different set of variations.

See you in the next part of this tutorial series.

All done!