Entrée

We saw in the previous part — django rest filtering tutorial part 2 how to filter querysets with the current user.

Let’s pickup where we left, but this time instead of current user we will the ability to filter against URL by any users we want.

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.

My example will be to filter authors by user #3, so my query will be look like this:

# httpie
$ http :8000/authors/3
# or with curl
# $ curl http://localhost:8000/authors/3

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

Where 3 is the user’s id/pk. If no user provided in the URL, the endpoint will just return all objects.

The Code

User ID

In order to achieve that we need to build our URL endpoint regular expressions like below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# authors/urls.py
from django.urls import path
from src.authors import views


urlpatterns = [
    path(
        "authors/", views.AuthorViewSet.as_view(actions={"get": "list"}), name="authors"
    ),
    path(
        "authors/<int:user>/",
        views.AuthorViewSet.as_view(actions={"get": "list"}),
        name="authors-by-users",
    ),
]

In the first path (line 7-9), we return all objects.

In the second path (line 10-14), we catch user by regex via Django’s path converter. Django will assign it as key:value map to View’s kwargs.

Info

In Django the below path converters are available by default:

  • str - Matches any non-empty string, excluding the path separator, '/'. This is the default if a converter isn’t included in the expression.
  • int - Matches zero or any positive integer. Returns an int.
  • slug - Matches any slug string consisting of ASCII letters or numbers, plus the hyphen and underscore characters. For example, something-your-drf-1-endpoint-something.
  • uuid - Matches a formatted UUID. To prevent multiple URLs from mapping to the same page, dashes must be included and letters must be lowercase. For example, 075194d3-6885-417e-a8a8-6c931e272f00. Returns a UUID instance.
  • path - Matches any non-empty string, including the path separator, '/'. This allows you to match against a complete URL path rather than just a segment of a URL path as with str.

Beside above pre-defined converters you can use your own regex if you need more complex cases like r'^comments/(?:page-(?P<page_number>\d+)/)?$' — if you are really sure about it: xkcd regex.

Also you can also build your own path converter: custom path converters.

You can get more information about URL dispatchers in offical Django Documention: django URL dispatcher.

Accordingly our related view AuthorViewSet will be like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# authors/views.py
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):
        user = self.kwargs.get("user", None)
        if user is not None:
            return Author.objects.filter(user=user)

        return super().get_queryset()

In this view we override default get_queryset method to get the user id. If no kwarg provided in the URL, we return the default queryset (line 18) which is Author.objects.all() (line 11).

If you use python >= 3.8, you can make it cleaner with walrus operator, PEP 572:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# authors/views.py
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 (user := self.kwargs.get("user", None)) is not None:
            return Author.objects.filter(user=user)

        return super().get_queryset()

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}"

User Name

Instead of user id, let’s filter by the name of the user:

$ http :8000/authors/UserName3

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

Update our urls.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# authors/urls.py
from django.urls import path
from src.authors import views


urlpatterns = [
    path(
        "authors/", views.AuthorViewSet.as_view(actions={"get": "list"}), name="authors"
    ),
    path(
        "authors/<str:name>/",
        views.AuthorViewSet.as_view(actions={"get": "list"}),
        name="authors-by-name",
    ),
]

And modify our views.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# authors/views.py
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):
        if (name := self.kwargs.get("name", None)) is not None:
            return Author.objects.filter(user__name__icontains=name)

        return super().get_queryset()

Any Name

Lastly, let’s change filtering by allowing to search the name in any of the below model field:

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

Modify our views.py to get the desired outputs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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):
        qs = super().get_queryset()
        if (name := self.kwargs.get("name", None) is not None):
            qs =  Author.objects.filter(
                Q(first_name__icontains=name)
                | Q(last_name__icontains=name)
                | Q(user__name__icontains=name)
                | Q(user__username__icontains=name)
            )

        return qs

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

Conclusion

In this tutorial we learned how to filter against the URL with the variations of defining different set of filters and keyword arguments.

See you in the next part of this tutorial series.

All done!