Django REST Filtering Tutorial - Filter with Current User (Part II)

serhatteker

Serhat Teker

Posted on April 18, 2022

Django REST Filtering Tutorial - Filter with Current User (Part II)

Entrée

We've seen in part I how to setup our repo, sending HTTP requests and some fundamental concepts about Django REST filtering.

In this second part we will limit our views/querysets/objects with user, more precisely with current authenticated user.

The authors list endpoint — /authors/, displays all items regardless of whether they belong to current user or not.

$ http :8000/authors

[
    {
        "first_name": "Name1",
        "full_name": "Name1 Surname1",
        "id": 1,
        "last_name": "Surname1",
        "user": 1
    },
    {
        "first_name": "Name2",
        "full_name": "Name2 Surname2",
        "id": 2,
        "last_name": "Surname2",
        "user": null
    },
    {
        "first_name": "Name3",
        "full_name": "Name3 Surname3",
        "id": 3,
        "last_name": "Surname3",
        "user": 3
    }
]
Enter fullscreen mode Exit fullscreen mode

How can we change that behavior in order to return items only owned by current authenticated user?

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.

Current User

This is one common filter usage which is filtering the queryset to ensure only relevant results to the current authenticated user who makes the request are returned.

For this purpose we will override default get_queryset as we've seen in default filtering in detail.

Let's filter authors list endpoint with the current user:

from rest_framework import generics

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


class AuthorListView(generics.ListAPIView):
    serializer_class = AuthorSerializer

    def get_queryset(self):
        """
        This view returns a list of all the authors for the currently
        authenticated user.

        Returns empyt list if user Anonymous
        """
        user = self.request.user

        if not user.is_anonymous:
            return Author.objects.filter(user=user)

        return Author.objects.none()
Enter fullscreen mode Exit fullscreen mode

NOTE

In our repo we use ModelViewSet for authors endpoint. You can update it like above.

You can look at related AuthorSerializer and Author model in the repo, however for easy access I put them there as well. In order to examine them just expand the below section.

AuthorSerializer and Author model

AuthorSerializer from src/authors/serializers.py:

# src/authors/serializers.py
from rest_framework import serializers

from src.authors.models import Author


class AuthorSerializer(serializers.ModelSerializer):

    class Meta:
        model = Author
        fields = ("id", "user", "first_name", "last_name", "full_name")
        read_only_fields = ("full_name",)
Enter fullscreen mode Exit fullscreen mode

Author model from src/authors/model.py:

# src/authors/model.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 unter a Pen Name
    """
    user = models.ForeignKey(to=USER, on_delete=models.DO_NOTHING, 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}"
Enter fullscreen mode Exit fullscreen mode

Now we can make a successful request by including the username and password of one of the users we created in populate_db.py:

$ http -a "User1:1234" ":8000/authors/"

[
    {
        "first_name": "Name1",
        "full_name": "Name1 Surname1",
        "id": 1,
        "last_name": "Surname1",
        "user": 1
    },
]
Enter fullscreen mode Exit fullscreen mode

As you can recognize now it returns only one object instead of three.

And if you we make a request as anonymous or unauthenticated user, we will get:

$ http ":8000/authors/"

[]
Enter fullscreen mode Exit fullscreen mode

User Authentication and Permission

It's usually better idea to separate authentication and permission layer/logic. For this goal you can use authentication_classes and permission_classes on per-view or per-viewset basis;

from rest_framework import generics
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated

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


class AuthorListView(generics.ListAPIView):
    serializer_class = AuthorSerializer
    authentication_classes = (SessionAuthentication, BasicAuthentication)
    permission_classes = (IsAuthenticated,)

    def get_queryset(self):
        """
        This view returns a list of all the authors for the currently
        authenticated user.
        """
        user = self.request.user

        return Author.objects.filter(user=user)
Enter fullscreen mode Exit fullscreen mode

Another method is setting the default authentication globally, using the DEFAULT_AUTHENTICATION_CLASSES setting:

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.BasicAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ]
}
Enter fullscreen mode Exit fullscreen mode

INFO

In the repo these are disabled, you can set them globally if you comment out the related lines at the end of the setting.py file.

And now our view will look cleaner like this:

# src/authors/views.py
from rest_framework import generics

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


class AuthorListView(generics.ListAPIView):
    serializer_class = AuthorSerializer

    def get_queryset(self):
        """
        This view returns a list of all the authors for the currently
        authenticated user.
        """
        return Author.objects.filter(user=self.request.user)
Enter fullscreen mode Exit fullscreen mode

For more info about authentication and permissions you can look at:

Custom FilterBackend

There is another method which is that we can also provide our own generic filtering backend.

To do so override BaseFilterBackend class, and override the .filter_queryset method. The method should return a new, filtered queryset.

As well as allowing clients to perform searches and filtering, generic filter backends can be useful to restrict which objects should be visible to any given request or user.

Let's write IsOwnerFilterBackend for our current user logic:

# src/authors/filters.py
from django_filters import rest_framework as filters


class IsOwnerFilterBackend(filters.BaseFilterBackend):
    """
    Filter that only allows users to get their own objects
    """
    def filter_queryset(self, request, queryset, view):
        return queryset.filter(owner=request.user)

Enter fullscreen mode Exit fullscreen mode

Add this filter to our AuthorListView:

# src/authors/views.py
from rest_framework import generics
from django_filters import rest_framework as filters

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


class AuthorListView(generics.ListAPIView):
    serializer_class = AuthorSerializer
    queryset = Author.objects.all()
    filter_backends = (IsOwnerFilterBackend,)
    filterset_fields = ("user",)
Enter fullscreen mode Exit fullscreen mode

Now if we send request we only receive related authors to the user.

One side note to this method; as we've already seen above, we can achieve the same behavior by overriding get_queryset() on the view, but using a filter backend allows us to more easily add this restriction to multiple views, or to apply it across the entire API.

We can also make this filter backend as our global default backend:

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_FILTER_BACKENDS": ["src.authors.filters.IsOwnerFilterBackend"],
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this tutorial we learned how to filter the queryset to ensure only relevant results to the current authenticated user who makes the request are returned.

See you in the next part of this tutorial series.

All done!

💖 💪 🙅 🚩
serhatteker
Serhat Teker

Posted on April 18, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related