Generic ViewSets — Custom Mixins
Welcome back! or Hey there! depending on whether you’re following the series or using this as a reference… both of which is perfectly alright by the way, you don’t need a lot of context to understand the crux of this article.
If you really only want to see the syntax or the rudimentary application of a mixin
, just scroll all the way down to Step 3: The Mixin. It’s towards the end of the article.
So let’s get started!
Like every time, here is a list of prerequisites that you should be familiar to truly appreciate this article:
- Generic ViewSets — You can check out the docs or our article here.
- Custom Endpoints — Again, the docs… or our article here.
- AbstractUser — Not completely required, but you’ll be more aware of what’s happening.
- ModelSerializers.
Do consider paying a visit to our publication if you wish to understand the project thoroughly.
I believe that the best place to start is to see where we want to end up, so…
Here’s our aim:
We’re trying to replicate Medium’s profile page backend.
Which means we need to retrieve and update the users information and their stories. We’re avoiding anything too complicated, so don’t worry about this too much.
This time, we will build the endpoints to retrieve the current users Stories and then create a mixin
to reduce code.
Here is the page we are building:
Some Background:
Last time, we created a custom endpoint and called it the me
endpoint. It returned the data of the current user. Here is the view
, the serializer
, the model
and the urls
for the same. Just go through it, we don’t need to be familiar with every bit of this code:
The Model:
The View:
The Serializer:
The URLs:
If you really wish to understand the project better, here it is on Github.
Let’s Start:
Now that everyone is on the same page, let’s get to what we want to do:
- Introduce the Stories
Model
. - Create an endpoint to get the current users stories.
- Create a custom
mixin
to reduce the amount of code.
Step 1: The Model -
… The title of this series is Django Rest For NOT Beginners…
Here it is :
Pro Tip:
A
Meta
class is a class, generally inside another class, that contains metadata about the parent class.The default behavior of Django is to just add an ‘s’ at the end of the name of the model to get the plural name.
In our case, we are using the
Meta
class to edit the metadata -the plural form of the name of this model. You can also use the Meta class to modify other metadata likeordering
ordb_index
.This change is purely cosmetic, it will make the name of the model appear properly in the Admin Panel that comes with Django.
Step 2: The Endpoint -
We’ve got our model
in place, now let’s look at the view
. We will be creating a new Generic ViewSet
for our stories.
This is what it looks like:
The Serializer:
The View:
Let’s break it down:
- We added a new
Serializer
calledStorySerializer
. It’s a simpleModelSerializer
. - We added a new
ViewSet
calledStoryViewSet
. - We created a custom endpoint called
me
that returns the current users stories. In order to do that:
- We got ourserializer
- We passed in the current user’s stories (We used therelated name
that links theUser model
to theStories model
)
-We mentionedmany=True
that tells theserializer
to expect multiple objects, because each user could have authored multiple stories. - We returned the desired data as a
Response
Notice how the two me
endpoints are really similar. We are violating a very important principle of development: DRY. It stands for Don’t Repeat Yourself. And that is what we will try to fix now.
So, lets try to make both the me
functions the same. Again, in steps.
- Find the difference between the two functions.
The two me
functions differ by only a single line. This one: data=serializer(request.user).data
— UserViewSetdata=serializer(request.user.stories, many=True)
— StoryViewSet
Even in this line, there’s really two things that differ. They are the arguments that we pass to the serializer
.
— To be able to change these arguments dynamically, we must first understand what they are.
2. Understand the arguments.
A serializer
takes many arguments. We perhaps delve into them in a later series. But what concerns us now are the instance
and the many
arguments.
- The
instance
takes in an object that needs to be serialized. - The
many
argument is a boolean that indicates if we are passing a single object or a list of objects (AQueryset
).
3. Abstract the arguments away.
Let’s get to the coding part.
First thing we must do is to create variables out of the arguments we pass.
This can get a little confusing, so try to code along. That always helps me understand.
- Create Variables Out Of Arguments:
Right now, we are passing data to our serializers
directly. Which is causing the two me
functions to be different. Which is why, we will now pass the data indirectly through variables.
# The UserViewSet me Endpoint@action(methods=['get'], detail=False)
def me(self, request):
serializer = self.get_serializer_class()
instance = request.user
many = False
data = serializer(instance = instance, many = many).data
return Response(data, status=status.HTTP_200_OK)# The StoryViewSet me Endpoint@action(methods=['get'], detail=False)
def me(self, request):
serializer = self.get_serializer_class()
instance = request.user.stories
many = True
data = serializer(instance = instance, many = many).data
return Response(data, status=status.HTTP_200_OK)
- Take those variables out of the function and make them properties of the parent class. The use the
self
keyword to access them.
class UserViewSet(GenericViewSet):
...
instance = request.user
many = False@action(methods=['get'], detail=False)
def me(self, request):
serializer = self.get_serializer_class()
data = serializer(
instance = self.instance,
many = self.many
).data
return Response(data, status=status.HTTP_200_OK)
class StoryViewSet(GenericViewSet):
... instance = request.user.stories
many = True@action(methods=['get'], detail=False)
def me(self, request):
serializer = self.get_serializer_class()
data = serializer(
instance = self.instance,
many = self.many
).data
return Response(data, status=status.HTTP_200_OK)
We’ve got ourselves an identical me
function. Yay!
But the above code won’t run just yet. The issue is that you cannot access the request
keyword outside a function. So how do we assign the instance as request.user
.
To solve this problem, we will create a function inside each of the two ViewSets
. This function simply returns the above two variables inside a dictionary. Let’s call this function: get_me_config
because, that’s what we’re doing… configuring the me
function.
Here is what the function will look like:
# This is for the UserViewSetdef get_me_config(self):
return {
'instance': self.request.user,
'many' : False
}
And now our me
function will have to be modified like this:
# This is for the UserViewSet but now the function is same for both@action(methods=['get'], detail=False)
def me(self, request):
serializer = self.get_serializer_class()
data = serializer(
instance=self.get_me_config().get('instance'),
many = self.get_me_config().get('many')
).data
return Response(data, status=status.HTTP_200_OK)
And we’re done with the abstraction.
Both our me
functions are now identical.
This has been heavy. So let’s take a breather.
Here is our final code after all the changes we just made:
So simple it looks right… after all the pain we just went through.
The me function is now clearly repeated. Let’s get rid of the repetition.
Step 3: The Mixin -
Well, after that life changer… turns out that, ironically, this section is the easiest… even though it’s the whole reason you are reading this article.
Making a mixin
is really simple. Here are the steps
- Make a class.
- Copy the repeating code.
- Include this new class in the
ViewSet
.
So let’s begin.
- Make A Class
class MeMixin:
Done.
2. Copy The Repeating Code
class MeMixin:
@action(methods=['get'], detail=False)
def me(self, request):
serializer = self.get_serializer_class()
data = serializer(
**self.get_me_config()
).data
return Response(data, status=status.HTTP_200_OK)
Pro Tip: using
**
before adictionary
or a function call that returns adictionary
will basically allow the function to use thedictionary
contents as arguments.
Done.
3. Include The New Mixin
in the ViewSet
class UserViewSet(MeMixin, GenericViewSet): serializer_class = UserSerializer
permission_classes = [IsAuthenticated] def get_queryset(self):
return User.objects.all()
Ta-da!
This time we’re done for real.
Here is the final code.
Does this look good? : Yes
Was it worth the pain? : Probably
This situation was probably not one in which you would consider using a custom mixin
. We didn’t save a whole bunch of lines by doing what we did.
But I chose this example because the relative ease of the aim let us focus on what was really important. Creating a Mixin
and messing around with it.
This one was probably a little more complex than the other articles we’ve been through and I hope I was able to convey the information across well.
Let’s wrap it up here. Next time, we will work multiple serializers
in a single viewset
. I hope to see you there.