3

Social Media Post Management

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:

  • Creating the Post model
  • Writing the Post model
  • Writing the Post serializer
  • Writing Post viewsets
  • Adding permissions
  • Deleting and updating posts
  • Adding the Like feature

Technical requirements

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.

Creating the Post model

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:

  • Authenticated users should be able to create a post
  • Authenticated users should be able to like the post
  • All users should be able to read the post, even if they aren’t authenticated
  • The author of the post should be able to modify the post
  • The author of the post should be able to delete the post

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.

Designing the Post model

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

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

Figure 3.2 – User and Post relationship

There are also two other types of database relationships:

  • One-to-one: In this type of relationship, a row in table A can only have one matching row in table B, and vice versa. An example of this can be worker C having one and only one desk D. And this desk D can only be used by this worker C (Figure 3.3):
Figure 3.3 – One-to-one relationship between a worker and a desk

Figure 3.3 – One-to-one relationship between a worker and a desk

  • Many-to-many: In this type of database relationship, a row in table A can have many matching rows in table B, and vice versa. For example, in an e-commerce application, an order can have many items, and an item can also appear in many different orders (Figure 3.4):
Figure 3.4 – Many-to-many relationship between an order and an item

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.

Abstraction

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.

Writing the AbstractSerializer

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.

Writing the AbstractViewSet

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:

  • filter_backends: This sets the default filter backend.
  • ordering_fields: This list contains the fields that can be used as ordering parameters when making a request.
  • ordering: This will tell Django REST in which order to send many objects as a response. In this case, all the responses will be ordered by the most recently updated.

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.

Writing the Post model

We have already established the structure of the Post model. Let’s write the code and the features:

  1. Create a new application called post:
    django-admin startapp post
  2. Rewrite apps.py of the new create package so it can be called easily in the project:

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"
  1. Once it’s done, we can now write the Post model. Open the models.py file and enter the following content:

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:

  • SET_NULL: This will set the child object foreign key to null on delete. For example, if a user is deleted from the database, the value of the author field of the posts in relation to this user is set to None.
  • SET_DEFAULT: This will set the child object to the default value given while writing the model. It works if you are sure that the default value won’t be deleted.
  • RESTRICT: This raises RestrictedError under certain conditions.
  • PROTECT: This prevents the foreign key object from being deleted as long as there are objects linked to the foreign key object.

Let’s test the newly added model by creating an object and saving it in the database:

  1. Add the newly created application to the INSTALLED_APPS list:

CoreRoot/settings.py

…
'core.post'
…
  1. Let’s create the migrations for the newly added application:
    python manage makemigrations && python manage.py migrate
  2. Then, let’s play with the Django shell by starting it with the python manage.py shell command:
    (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.

  1. Let’s import a user. This will be the author of the post we’ll be creating:
    >>> from core.post.models import Post
    >>> from core.user.models import User
    >>> user = User.objects.first()
    >>> user
  2. Next, let’s create a dictionary that will contain all the fields needed to create a post:
    >>> data = {"author": user, "body":"A simple test"}
  3. And now, let’s create a post:
    >>> post = Post.objects.create(**data)
    >>> post
    <Post: John Hey>
    >>>
    Let's access the author field of this object.
    >>> post.author

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.

Writing the Post serializer

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.

Writing 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:

  • Only authenticated users can create posts
  • Only authenticated users can read posts
  • Only GET and POST methods are allowed

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:

  • The get_queryset method returns all the posts. We don’t actually have particular requirements for fetching posts, so we can return all posts in the database.
  • The get_object method returns a post object using public_id that will be present in the URL. We retrieve this parameter from the self.kwargs directory.
  • The create method, which is the ViewSet action executed on POST requests on the endpoint linked to ViewSet. We simply pass the data to the serializer declared on ViewSet, validate the data, and then call the perform_create method to create a post object. This method will automatically handle the creation of a post object by calling the Serializer.create method, which will trigger the creation of a post object in the database. Finally, we return a response with the newly created post.

And right here, you have the code for ViewSet. The next step is to add an endpoint and start testing the API.

Adding the Post route

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

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

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

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.

Rewriting the Post serialized 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

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.

Adding permissions

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:

  • The anonymous user: This user has no account on the API and can’t really be identified
  • The registered and active user: This user has an account on the API and can easily perform some actions
  • The admin user: This user has all rights and privileges

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 posts

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

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

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.

Adding the Like feature

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:

  1. Add a new posts_liked field to the User model.
  2. Write methods on the User model to like and remove a like from a post. We’ll also add a method to check whether the user has liked a post.
  3. Add likes_count and has_liked to PostSerializer.
  4. Add endpoints to like and dislike a post.

Great! Let’s start by adding the new fields to the User model.

Adding the posts_liked field 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:

  • A user can like many posts
  • A post can be liked by many users

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

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.

Adding the like, remove_like, and has_liked methods

Before writing these methods, let’s describe the purpose of each new method:

  • The like() method: This is used for liking a post if it hasn’t been done yet. For this, we’ll use the add() method from the models. We’ll use ManyToManyField to link a post to a user.
  • The remove_like() method: This is used for removing a like from a post. For this, we’ll use the remove method from the models. We’ll use ManyToManyField to unlink a post from a user.
  • The has_liked() method: This is used for returning True if the user has liked a post, else False.

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.

Adding 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.

Adding like and dislike actions to PostViewSet

DRF provides a decorator called action. This decorator helps make methods on a ViewSet class routable. The action decorator takes two arguments:

  • detail: If this argument is set to True, the route to this action will require a resource lookup field; in most cases, this will be the ID of the resource
  • methods: This is a list of the methods accepted by the action

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:

  1. First, we retrieve the concerned post on which we want to call the like or remove the like action. The self.get_object() method will automatically return the concerned post using the ID passed to the URL request, thanks to the detail attribute being set to True.
  2. Second, we also retrieve the user making the request from the self.request object. This is done so that we can call the remove_like or like method added to the User model.
  3. And finally, we serialize the post using the Serializer class defined on self.serializer_class and we return a response.

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:

  1. Like a post with the following endpoint: api/post/post_pk/like/.
  2. Remove the like from a post with the following endpoint: api/post/post_pk/remove_like/.

Great, the feature is working like a charm. In the next chapter, we’ll be adding the comments feature to the project.

Summary

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.

Questions

  1. What are some database relationships?
  2. What are Django permissions?
  3. How do you paginate the results of an API response?
  4. How do you use Django shell?
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset