In the previous chapter, we introduced models, serializers, viewsets, and routes to create our first endpoints. In this chapter, we will be working with the same concepts for creating posts for our social media project. This will be done by dividing the project into concepts such as database relations, filtering, and permissions. By the end of this chapter, you’ll be able to work with database relations with Django models, write custom filters and permissions, and delete and update objects.
We will be covering the following topics in this chapter:
For this chapter, you need to have Insomnia installed on your machine to make HTTP requests.
You can find the code for this chapter here: https://github.com/PacktPublishing/Full-stack-Django-and-React/tree/chap3.
A post in this project is a long or short piece of text that can be viewed by anyone, irrespective of whether a user is linked or associated to that post. Here are the requirements for the post feature:
Looking at these requirements from a backend perspective, we can understand that we’ll be dealing with a database, a model, and permissions. First, let’s start by writing the structure of the Post model in the database.
A post consists of content made up of characters written by an author (here, a user). How does that schematize itself into our database?
Before creating the Post model, let’s draw a quick figure of the structure of the model in the database:
Figure 3.1 – Post table
As you can see in Figure 3.1, there is an author field, which is a foreign key. A foreign key is a set of attributes in a table that refers to the primary key of another table. In our case, the foreign key will refer to the primary key of the User table. Each time a post is created, a foreign key will need to be passed.
The foreign key is one of the characteristics of the one-to-many (or many-to-one) relationship. In this relationship, a row in table A can have many matching rows in table B (one-to-many) but a row in table B can only have one matching row in table A.
In our case, a user (from the User table) can have many posts (in the Post table) but a post can only have one user (Figure 3.2):
Figure 3.2 – User and Post relationship
There are also two other types of database relationships:
Figure 3.3 – One-to-one relationship between a worker and a desk
Figure 3.4 – Many-to-many relationship between an order and an item
The many-to-many relationship will be used when writing the like feature for the posts.
Great, now that we have a better idea of database relationships, we can begin to write the post feature, starting from the Post model. But before that, let’s quickly refactor the code to make development easier.
The next models that we’ll create will also have the public_id, created, and updated fields. For the sake of the don’t repeat yourself (DRY) principle, we will use abstract model classes.
An abstract class can be considered a blueprint for other classes. It usually contains a set of methods or attributes that must be created within any child classes built from the abstract class.
Inside the core directory, create a new Python package called abstract. Once it’s done, create a models.py file. In this file, we will write two classes: AbstractModel and AbstractManager.
The AbstractModel class will contain fields such as public_id, created, and updated. On the other side, the AbstractManager class will contain the function used to retrieve an object by its public_id field:
core/abstract/models.py
from django.db import models import uuid from django.core.exceptions import ObjectDoesNotExist from django.http import Http404 class AbstractManager(models.Manager): def get_object_by_public_id(self, public_id): try: instance = self.get(public_id=public_id) return instance except (ObjectDoesNotExist, ValueError, TypeError): return Http404 class AbstractModel(models.Model): public_id = models.UUIDField(db_index=True, unique=True, default=uuid.uuid4, editable=False) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) objects = AbstractManager() class Meta: abstract = True
As you can see in the Meta class for AbstractModel, the abstract attribute is set to True. Django will ignore this class model and won’t generate migrations for this.
Now that we have this class, let’s make a quick refactor on the User model:
First, let’s remove the get_object_by_public_id method to retrieve an object via public_id, and let’s subclass UserManager:
core/user/models.py
… from core.abstract.models import AbstractModel, AbstractManager class UserManager(BaseUserManager, AbstractManager): … class User(AbstractModel, AbstractBaseUser, PermissionsMixin): …
On the User model, remove the public_id, updated, and created fields, and also, subclass the User model with the AbstractModel class. This will normally cause no changes to the database, hence, there is no need to run makemigrations again unless you’ve changed an attribute of a field.
Let’s also add AbstractSerializer, which will be used by all the serializers we’ll be creating on this project.
All the objects sent back as a response in our API will contain the id, created, and updated fields. It’ll be repetitive to write these fields all over again on every ModelSerializer, so let’s just create an AbstractSerializer class. In the abstract directory, create a file called serializers.py and add the following content:
core/abstract/serializers.py
from rest_framework import serializers class AbstractSerializer(serializers.ModelSerializer): id = serializers.UUIDField(source='public_id', read_only=True, format='hex') created = serializers.DateTimeField(read_only=True) updated = serializers.DateTimeField(read_only=True)
Once it’s done, you can go and subclass the UserSerializer class with the AbstractSerializer class:
core/user/serializers.py
from core.abstract.serializers import AbstractSerializer from core.user.models import User class UserSerializer(AbstractSerializer): …
Once it’s done, remove the field declaration of id, created, and updated.
Let’s perform one last abstraction for ViewSets.
But why write an abstract ViewSet? Well, there will be repeated declarations as to the ordering and the filtering. Let’s create a class that will contain the default values.
In the abstract directory, create a file called viewsets.py and add the following content:
core/abstract/viewsets.py
from rest_framework import viewsets from rest_framework import filters class AbstractViewSet(viewsets.ModelViewSet): filter_backends = [filters.OrderingFilter] ordering_fields = ['updated', 'created'] ordering = ['-updated']
As you can see, we have the following attributes:
The next step is to add the AbstractViewSet class to the code where ModelViewSets is actually called. Go to core/user/viewsets.py and subclass UserViewSet with the AbstractViewSet class:
core/user/viewsets.py
… from core.abstract.viewsets import AbstractViewSet from core.user.serializers import UserSerializer from core.user.models import User class UserViewSet(AbstractViewSet): …
Great, now we have all the things needed to write better and less code; let’s write the Post model.
We have already established the structure of the Post model. Let’s write the code and the features:
django-admin startapp post
core/post/apps.py
from django.apps import AppConfig class PostConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'core.post' label = "core_label"
core/post/models.py
from django.db import models from core.abstract.models import AbstractModel, AbstractManager class PostManager(AbstractManager): pass class Post(AbstractModel): author = models.ForeignKey(to="core_user.User", on_delete=models.CASCADE) body = models.TextField() edited = models.BooleanField(default=False) objects = PostManager() def __str__(self): return f"{self.author.name}" class Meta: db_table = "'core.post'"
You can see here how we created the ForeignKey relationship. Django models actually provide tools to handle this kind of relationship, and it’s also symmetrical, meaning that not only can we use the Post.author syntax to access the user object but we can also access posts created by a user using the User.post_set syntax. The latter syntax will return a queryset object containing the posts created by the user because we are in a ForeignKey relationship, which is also a one-to-many relationship. You will also notice the on_delete attribute with the models.CASCADE value. Using CASCADE, if a user is deleted from the database, Django will also delete all records of posts in relation to this user.
Apart from CASCADE as a value for the on_delete attribute on a ForeignKey relationship, you can also have the following:
Let’s test the newly added model by creating an object and saving it in the database:
CoreRoot/settings.py
… 'core.post' …
python manage makemigrations && python manage.py migrate
(venv) koladev@koladev123xxx:~/PycharmProjects/Full-stack-Django-and-React$ python manage.py shell
Python 3.10.2 (main, Jan 15 2022, 18:02:07) [GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>
Important note
You can use the django_shell_plus package to speed up work with Django shell. You won’t need to type all imports yourself as all your models will be imported by default. You can find more information on how to install it from the following website: https://django-extensions.readthedocs.io/en/latest/shell_plus.html.
>>> from core.post.models import Post
>>> from core.user.models import User
>>> user = User.objects.first()
>>> user
>>> data = {"author": user, "body":"A simple test"}
>>> post = Post.objects.create(**data)
>>> post
<Post: John Hey>
>>>
Let's access the author field of this object.
>>> post.author
<User: [email protected]>
As you can see, the author is in fact the user we’ve retrieved from the database.
Let’s also try the inverse relationship:
>>> user.post_set.all() <QuerySet [<Post: John Hey>]>
As you can see, the post_set attribute contains all the instructions needed to interact with all the posts linked to this user.
Now that you have a better understanding of how database relationships work in Django, we can move on to writing the serializer of the Post object.
The Post serializer will contain the fields needed to create a post when making a request on the endpoint. Let’s add the feature for the post creation first.
In the post directory, create a file called serializers.py. Inside this file, add the following content:
core/post/serializers.py
from rest_framework import serializers from rest_framework.exceptions import ValidationError from core.abstract.serializers import AbstractSerializer from core.post.models import Post from core.user.models import User class PostSerializer(AbstractSerializer): author = serializers.SlugRelatedField( queryset=User.objects.all(), slug_field='public_id') def validate_author(self, value): if self.context["request"].user != value: raise ValidationError("You can't create a post for another user.") return value class Meta: model = Post # List of all the fields that can be included in a # request or a response fields = ['id', 'author', 'body', 'edited', 'created', 'updated'] read_only_fields = ["edited"]
We’ve added a new serializer field type, SlugRelatedField. As we are working with the ModelSerializer class, Django automatically handles the fields and relationship generation for us. Defining the type of relationship field we want to use can also be crucial to tell Django exactly what to do.
And that’s where SlugRelatedField comes in. It is used to represent the target of the relationship using a field on the target. Thus, when creating a post, public_id of the author will be passed in the body of the request so that the user can be identified and linked to the post.
The validate_author method checks validation for the author field. Here, we want to make sure that the user creating the post is the same user as in the author field. A context dictionary is available in every serializer. It usually contains the request object that we can use to make some checks.
There is no hard limitation here so we can easily move to the next part of this feature: writing the Post viewsets.
For the following endpoint, we’ll only be allowing the POST and GET methods. This will help us have the basic features working first.
The code should follow these rules:
Inside the post directory, create a file called viewsets.py. Into the file, add the following content:
core/post/viewsets.py
from rest_framework.permissions import IsAuthenticated from core.abstract.viewsets import AbstractViewSet from core.post.models import Post from core.post.serializers import PostSerializer class PostViewSet(AbstractViewSet): http_method_names = ('post', 'get') permission_classes = (IsAuthenticated,) serializer_class = PostSerializer def get_queryset(self): return Post.objects.all() def get_object(self): obj = Post.objects.get_object_by_public_id( self.kwargs['pk']) self.check_object_permissions(self.request, obj) return obj def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) return Response(serializer.data, status=status.HTTP_201_CREATED)
In the preceding code, we defined three interesting methods:
And right here, you have the code for ViewSet. The next step is to add an endpoint and start testing the API.
In the routers.py file, add the following content:
core/routers.py
… from core.post.viewsets import PostViewSet # ##################################################################### # # ################### POST ###################### # # ##################################################################### # router.register(r'post', PostViewSet, basename='post') …
Once it’s done, you’ll have a new endpoint available on /post/. Let’s play with Insomnia to test the API.
First of all, try to make a request directly to the /post/ endpoint. You’ll receive a 401 error, meaning that you must provide an access token. No problem, log in on the /auth/login/ endpoint with a registered user and copy the token.
In the Bearer tab in Insomnia, select Bearer Token:
Figure 3.5 – Adding Bearer Token to Insomnia request
Now, fire the endpoint again with a GET request. You’ll see no results, great! Let’s create the first post in the database.
Change the type of request to POST and the following to the JSON body:
{ "author": "19a2316e94e64c43850255e9b62f2056", "body": "A simple posted" }
Please note that we will have a different public_id so make sure to use public_id of the user you’ve just logged in as and send the request again:
Figure 3.6 – Creating a post
Great, the post is created! Let’s see whether it’s available when making a GET request:
Figure 3.7 – Getting all posts
The DRF provides a way to paginate responses and a default pagination limit size globally in the settings.py file. With time, a lot of objects will be shown and the size of the payload will vary.
To prevent this, let’s add a default size and a class to paginate our results.
Inside the settings.py file of the project, add new settings to the REST_FRAMEWORK dictionary:
CoreRoot/settings.py
REST_FRAMEWORK = { … 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 'PAGE_SIZE': 15, } …
Basically here, all results are limited to 15 per page but we can also increase this size with the limit parameter when making a request and also use the offset parameter to precisely where we want the result to start from:
GET https://api.example.org/accounts/?limit=100&offset=400
Great, now make a GET request again and you’ll see that the results are better structured.
Also, it’ll be more practical to have the name of the author in the response as well. Let’s rewrite a serializer method that can help modify the response object.
Actually, the author field accepts public_id and returns public_id. While it does the work, it can be a little bit difficult to identify the user. This will cause it to make a request again with public_id of the user to get the pieces of information about the user.
The to_representation() method takes the object instance that requires serialization and returns a primitive representation. This usually means returning a structure of built-in Python data types. The exact types that can be handled depend on the render classes you configure for your API.
Inside post/serializers.py, add a new method called to_represenation():
core/post/serializers.py
class PostSerializer(AbstractSerializer): … def to_representation(self, instance): rep = super().to_representation(instance) author = User.objects.get_object_by_public_id( rep["author"]) rep["author"] = UserSerializer(author).data return rep …
As you can see, we are using the public_id field to retrieve the user and then serialize the User object with UserSerializer.
Let’s get all the posts again and you’ll see all the users:
Figure 3.8 – Getting all posts
We have a working Post feature but it also has some issues. Let’s explore this further when writing permissions for our feature.
If authentication is the action of verifying the identity of a user, authorization is simply the action of checking whether the user has the rights or privileges to perform an action.
In our project, we have three types of users:
We want anonymous users to be able to read the posts on the API without necessarily being authenticated. While it’s true that there is the AllowAny permission, it’ll surely conflict with the IsAuthenticated permission.
Thus, we need to write a custom permission.
Inside the authentication directory, create a file called permissions, and add the following content:
core/post/viewsets.py
from rest_framework.permissions import BasePermission, SAFE_METHODS class UserPermission(BasePermission): def has_object_permission(self, request, view, obj): if request.user.is_anonymous: return request.method in SAFE_METHODS if view.basename in ["post"]: return bool(request.user and request.user.is_authenticated) return False def has_permission(self, request, view): if view.basename in ["post"]: if request.user.is_anonymous: return request.method in SAFE_METHODS return bool(request.user and request.user.is_authenticated) return False
Django permissions usually work on two levels: on the overall endpoint (has_permission) and on an object level (has_object_permission).
A great way to write permissions is to always deny by default; that is why we always return False at the end of each permission method. And then you can start adding the conditions. Here, in all the methods, we are checking that anonymous users can only make the SAFE_METHODS requests — GET, OPTIONS, and HEAD.
And for other users, we are making sure that they are always authenticated before continuing. Another important feature is to allow users to delete or update posts. Let’s see how we can add this with Django.
Deleting and updating articles are also part of the features of posts. To add these functionalities, we don’t need to write a serializer or a viewset, as the methods for deletion (destroy()), and updating (update()) are already available by default in the ViewSet class. We will just rewrite the update method on PostSerializer to ensure that the edited field is set to True when modifying a post.
Let’s add the PUT and DELETE methods to http_methods of PostViewSet:
core/post/viewsets.py
… class PostViewSet(AbstractViewSet): http_method_names = ('post', 'get', 'put', 'delete') …
Before going in, let’s rewrite the update method in PostSerializer. We actually have a field called edited in the Post model. This field will tell us whether the post has been edited:
core/post/serializers.py
… class PostSerializer(AbstractSerializer): … def update(self, instance, validated_data): if not instance.edited: validated_data['edited'] = True instance = super().update(instance, validated_data) return instance …
And let’s try the PUT and DELETE requests in Insomnia. Here’s an example of the body for the PUT request:
{ "author": "61c5a1ecb9f5439b810224d2af148a23", "body": "A simple post edited" }
Figure 3.9 – Modifying a post
As you can see, the edited field in the response is set to true.
Let’s try to delete the post and see whether it works:
Figure 3.10 – Deleting a post
Important note
There is a way to delete records without necessarily deleting them from the database. It’s usually called a soft delete. The record just won’t be accessible to the user, but it will always be present in the database. You can learn more about this at https://dev.to/bikramjeetsingh/soft-deletes-in-django-a9j.
A nice feature to have in a social media application is favoriting. Like Facebook, Instagram, or Twitter, we’ll allow users here to like a post.
Plus, we’ll also add data to count the number of likes a post has received and check whether a current user making the request has liked a post.
We’ll do this in four steps:
Great! Let’s start by adding the new fields to the User model.
The posts_liked field will contain all the posts liked by a user. The relationship between the User model and the Post model concerning the Like feature can be described as follows:
This kind of relationship sounds familiar? It is a many-to-many relationship.
Following this change, here’s the updated structure of the table – we are also anticipating the methods we’ll add to the model:
Figure 3.11 – New User table structure
Great! Let’s add the posts_liked field to the User model. Open the /core/user/models.py file and add a new field to the User model:
class User(AbstractModel, AbstractBaseUser, PermissionsMixin): ... posts_liked = models.ManyToManyField( "core_post.Post", related_name="liked_by" ) ...
After that, run the following commands to create a new migrations file and apply this migration to the database:
python manage.py makemigrations python manage.py migrate
The next step is to add the new methods shown in Figure 3.11 to the User model.
Before writing these methods, let’s describe the purpose of each new method:
Let’s move on to the coding:
class User(AbstractModel, AbstractBaseUser, PermissionsMixin): ... def like(self, post): """Like `post` if it hasn't been done yet""" return self.posts_liked.add(post) def remove_like(self, post): """Remove a like from a `post`""" return self.posts_liked.remove(post) def has_liked(self, post): """Return True if the user has liked a `post`; else False""" return self.posts_liked.filter(pk=post.pk).exists()
Great! Next, let’s add the likes_count and has_liked fields to PostSerializer.
Instead of adding fields such as likes_count in the Post model and generating more fields in the database, we can directly manage it on PostSerializer. The Serializer class in Django provides ways to create the write_only values that will be sent on the response.
Inside the core/post/serializers.py file, add new fields to PostSerializer:
Core/post/serializers.py
... class PostSerializer(AbstractSerializer): ... liked = serializers.SerializerMethodField() likes_count = serializers.SerializerMethodField() def get_liked(self, instance): request = self.context.get('request', None) if request is None or request.user.is_anonymous: return False return request.user.has_liked(instance) def get_likes_count(self, instance): return instance.liked_by.count() class Meta: model = Post # List of all the fields that can be included in a # request or a response fields = ['id', 'author', 'body', 'edited', 'liked', 'likes_count', 'created', 'updated'] read_only_fields = ["edited"]
In the preceding code, we are using the serializers.SerializerMethodField() field, which allows us to write a custom function that will return a value we want to attribute to this field. The syntax of the method will be get_field, where field is the name of the field declared on the serializer.
That is why for liked, we have the get_liked method, and for likes_count, we have the get_likes_count method.
With the new fields on PostSerializer, we can now add the endpoints needed to PostViewSet to like or dislike an article.
DRF provides a decorator called action. This decorator helps make methods on a ViewSet class routable. The action decorator takes two arguments:
Let’s write the actions on PostViewSets:
core/post/viewsets.py
... class PostViewSet(AbstractViewSet): ... @action(methods=['post'], detail=True) def like(self, request, *args, **kwargs): post = self.get_object() user = self.request.user user.like(post) serializer = self.serializer_class(post) return Response(serializer.data, status=status.HTTP_200_OK) @action(methods=['post'], detail=True) def remove_like(self, request, *args, **kwargs): post = self.get_object() user = self.request.user user.remove_like(post) serializer = self.serializer_class(post) return Response(serializer.data, status=status.HTTP_200_OK)
For each action added, we are writing the logic following these steps:
With this added to PostViewSets, the Django Rest Framework routers will automatically create new routes for this resource, and then, you can do the following:
Great, the feature is working like a charm. In the next chapter, we’ll be adding the comments feature to the project.
In this chapter, we’ve learned how to use database relationships and write permissions. We also learned how to surcharge updates and create methods on viewsets and serializers.
We performed quick refactoring on our code by creating an Abstract class to follow the DRY rule. In the next chapter, we’ll be adding the Comments feature on the posts. Users will be able to create comments under posts as well as delete and update them.