Build a Blog Website using Django Rest Framework — Part 3
In the third part, we deal with the entire posts app of our application, thus finishing the backend of our application
Hello folks, I hope you all are doing well and that you enjoyed the first two parts of this DRF article series. In the first part of the series, we dealt with the basics of setting up this project, and you all got an overview of the project we will build, while in the second part, we dealt with the users
app of our application where we wrote the serializer and view for our users
part
If you have not yet read those parts, I strongly suggest you go and first read the first two parts and then come back to this article.
In the third part of the series, we will deal with the complete posts
app of our Django application, thus finishing up the complete backend for our application. We would also be testing the APIs using the Browsable API interface in the same way we did for the users
app. So, let us dive right into the posts
app and start building it.
So, when we move into the posts
folder, we will see we have the same files as we did for the users
app when we moved into that folder. First, we would be moving into the models.py
file to build our models.
models.py
Let’s start with the models.py
file first. We will define our database models here. So, we will have three different models for the posts
part — Post
, Upvote
and Comment
.
We will first start with importing the required dependencies into our file, as shown below:
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
As we can see, we have imported the default User
model and also imported models
and reverse
from Django. These would help us in building our models.
So, we would start with the Posts
model. Let us first look into the code, and then we will understand the significance of those lines.
class Post(models.Model):
user = models.ForeignKey(User, on_delete = models.CASCADE)
title = models.CharField(max_length = 100)
body = models.TextField()
created = models.DateTimeField(auto_now_add = True)
updated = models.DateTimeField(auto_now = True)
upvote_count = models.IntegerField(default = 0)def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post-detail', kwargs = {'pk': self.pk})
The code above defines a Post
class that inherits from Django’s models.Model
class. This makes the Post
class a Django model, which allows it to be saved in and retrieved from a database.
The Post
class has several fields, including user
, title
, body
, created
, updated
, and upvote_count
. Each field is defined using a class from Django’s models
module, which specifies the type of data that each field will store. For example, the title
field is a CharField
, which will store the title of the blog post, and the created
field is a DateTimeField
, which stores the time at which the post was created.
The user
field is defined as a ForeignKey
field, which means that it is a reference to the default Django User
model. The on_delete
argument specifies what should happen to a Post
object if the User
object that it references is deleted. Here, the on_delete
argument is set to models.CASCADE
, which signifies that if the User
object is deleted, the Post
object will also be deleted.
The ForeignKey
field allows a Post
to be linked to a specific User
object, which can be accessed using the post.user
attribute. This can be used to get the username of the user who created the Post
, or to allow only the user who created the Post
to edit or delete it.
The upvote_count
field will store the number of upvotes the blog post gets from the users. The default value has been set to zero, which signifies that when a new Post
object is created, the upvote_count
is set to zero. The upvote_count
will be incremented when the user clicks on the upvote button on the blog post.
The __str__
method is a special method that defines how a Post
object should be represented as a string. In this case, the method returns the title
of the Post
.
The get_absolute_url
method returns the URL of the Post
object’s detail page. Django uses this to determine the URL of the object when it is saved to the database.
Next, we would look into the Upvote
model that will store all the upvotes in our application. Let us have a look at the model, and then I will explain the significance of the code.
class Upvote(models.Model):
user = models.ForeignKey(User, related_name = 'upvotes', on_delete = models.CASCADE)
post = models.ForeignKey(Post, related_name = 'upvotes', on_delete = models.CASCADE)
This code defines an Upvote
class that inherits from Django’s models.Model
class, just like the Post
class that we discussed earlier.
The Upvote
class has two fields: user
and post
. Both fields have been defined as ForeignKey
fields, which signifies that they are references to User
and Post
objects, respectively. The related_name
argument specifies the name that should be used to access the reverse relationship. For example, if a User
object user
has upvoted several Post
objects, the user.upvotes
attribute will return a queryset of the Post
objects that user
has upvoted.
The on_delete
argument for both fields is set to models.CASCADE
, which means that if either the User
or the Post
object that an Upvote
object references is deleted, the Upvote
object will also be deleted.
This class is used to track which users have upvoted which posts. We will use this to prevent a user from upvoting a Post
more than once, or we can also use this to retrieve the list of users who have upvoted a given Post
, though we will not be implementing the latter in our application. But, as we have seen, it is easy to do so, and you can surely try to implement it to improve the application.
Next, we move to the final model class Comment
, which will store all the comments made on the blog posts in our application. The code for the same is below.
class Comment(models.Model):
user = models.ForeignKey(User, related_name = 'comments', on_delete = models.CASCADE)
post = models.ForeignKey(Post, related_name = 'comments', on_delete = models.CASCADE)
body = models.TextField()
created = models.DateTimeField(auto_now_add = True)def __str__(self):
return self.body
The code defines a Comment
class that inherits from Django’s models.Model
class, just like the Post
and Upvote
classes.
The Comment
class has four fields: user
, post
, body
and created
. The user
and post
fields are defined as ForeignKey
fields, which means that they are references to User
and Post
objects, respectively. The related_name
argument for both fields specifies the name that should be used to access the reverse relationship. For example, if a User
object user
has written several Comment
objects, the user.comments
attribute will return a queryset of the Comment
objects that user
has written.
The body
field is a TextField
, which stores the main content of the comment is stored.
The created
field is a DateTimeField
that stores the date and time when the Comment
object was created. The auto_now_add
argument is set to True
, which means that the Comment
object’s created
field will automatically be set to the current date and time when the object is first created.
The __str__
method is a special method that defines how a Comment
object should be represented as a string. In this case, the method returns the body
of the Comment
, which is the main content of the comment.
The Comment
class is used to determine which users have commented on a particular Post
object. We will use this to display the comments on a blog post along with the user’s name. Also, this can be used to allow only the user who created the comment to edit or delete it though we have not implemented this part in the current application.
The complete code for the models.py
section can be found below:
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverseclass Post(models.Model):
user = models.ForeignKey(User, on_delete = models.CASCADE)
title = models.CharField(max_length = 100)
body = models.TextField()
created = models.DateTimeField(auto_now_add = True)
updated = models.DateTimeField(auto_now = True)
upvote_count = models.IntegerField(default = 0)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post-detail', kwargs = {'pk': self.pk})
class Upvote(models.Model):
user = models.ForeignKey(User, related_name = 'upvotes', on_delete = models.CASCADE)
post = models.ForeignKey(Post, related_name = 'upvotes', on_delete = models.CASCADE)
class Comment(models.Model):
user = models.ForeignKey(User, related_name = 'comments', on_delete = models.CASCADE)
post = models.ForeignKey(Post, related_name = 'comments', on_delete = models.CASCADE)
body = models.TextField()
created = models.DateTimeField(auto_now_add = True)
def __str__(self):
return self.body
Next, we would be moving to serializers.py
file. Since this file would not be present by default, we need to create it as we did for the users
app.
serializers.py
In this file, we will serialize our models to make them in a format which can be shared across the web and can be accessed by any frontend framework.
First, we will be importing the required dependencies in our file. We will import serializers
from rest_framework
package, and also we import the three models we created — Post
, Upvote
and Comment
.
Next, we will write three serializers — one for each of the three models. The code for the serializers class is given below. Let us first look at the code, and then we can discuss it later.
from rest_framework import serializers
from .models import Post, Upvote, Commentclass PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ('title', 'body', 'created', 'updated', 'user', 'upvote_count')
class UpvoteSerializer(serializers.ModelSerializer):
class Meta:
model = Upvote
fields = ('user', 'post')
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ('user', 'post', 'body', 'created')
The structure is very similar to the serializer we wrote for the Users
model in the previous article.
Each of the three classes written above defines a serializer for a specific model: Post
, Upvote
, and Comment
. The Meta
inner class of each serializer class specifies the model that the serializer should work with and which fields of the model should be included in the serialized data.
As we can see, PostSerializer
will serialize instances of the Post
model and will include the title
, body
, created
, updated
, user
, and upvote_count
fields in the serialized data.
Similarly, UpvoteSerializer
and CommentSerializer
will serialize their own fields.
Next, we move to the most important part — views, where we would be defining all the logic of the application, and we will deal with get
, post
, put
and delete
requests in that file.
views.py
In this section, we will define various views
for handling different requests related to the three models, we have created.
We will be adding the following functionalities to our application:
- Seeing all the blog posts
- Creating a new blog post
- Editing a blog post written by that particular user
- Deleting a blog post written by that particular user
- Seeing the blog posts written by a specific user
- Upvoting the post
- Adding a comment on the post
So, let us start with the required imports.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework import permissions
from .models import Post, Upvote, Comment
from .serializers import PostSerializer, UpvoteSerializer, CommentSerializer
from django.contrib.auth.models import User
So, in the views.py
file, we have imported various classes and functions — some from rest_framework
package, while some are the serilializers
and models
.
APIView
is a class that provides a way to define views that handle incoming HTTP requests in a RESTful manner. A view is a callable that takes an HTTP request as its argument and returns an HTTP response.
Response
is a class that provides a way to create HTTP response objects. An HTTP response typically includes a status code, headers, and a body containing the response data.
status
is a module that defines a number of HTTP status codes as constants. These codes indicate the success or failure of an HTTP request.
permissions
is a module that provides classes for implementing permission checks in views. A permission is a rule that determines whether a user has the right to access a particular view.
The code also imports several model classes from the .models
module, as well as the serializer classes from the .serializers
module. Finally, it imports the User
class from the django.contrib.auth.models
module, which is a class that represents a user of the Django web framework, i.e. the default Django User
model class.
Also, all the classes in the views.py
file, we will require the user to be logged in, so we will use the permissions
module to enforce this in all the classes.
Next, we will define PostListAPIView
class which will provide the ability to fetch all the blog posts available and also will have the ability to create a new blog post. Here is the code for the same:
class PostListAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]def get(self, request, *args, **kwargs):
posts = Post.objects.all()
serializer = PostSerializer(posts, many = True)
return Response(serializer.data, status = status.HTTP_200_OK)
def post(self, request, *args, **kwargs):
data = {
'user': request.user.id,
'title': request.data.get('title'),
'body': request.data.get('body')
}
serializer = PostSerializer(data = data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status = status.HTTP_201_CREATED)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
This is a subclass of APIView
, which provides several methods for handling different HTTP request methods, such as GET
, POST
, PUT
, and DELETE
.
The permission_classes
attribute of the PostListAPIView
class specifies that only authenticated users are allowed to access this view. This is achieved by using the isAuthenticated
method. This means that only users who have logged in will be able to see the list of posts or create new posts. We will be using the same in all other classes too.
The get
method of the PostListAPIView
class is called when a GET
request is sent to the view. This method retrieves all the blog posts on the website and creates a serialized representation of the data using the PostSerializer
class. It then returns this data in an HTTP response with a status code of 200 OK.
The post
method of the PostListAPIView
class is called when a POST
request is sent to the view. This method creates a new Post
object using the data from the request, serializes the data using the PostSerializer
class, and returns the serialized data in an HTTP response with a status code of 201 CREATED. If the data from the request is not valid, it returns an error message with a status code of 400 BAD REQUEST.
Next, we define the PostDetailAPIView
which will deal with the operations of an individual post in a blog. It will handle fetching that particular post, editing it or deleting it. It also has the same permissions which we had for the previous class. Here is the code for it:
class PostDetailAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]def get_object(self, pk):
try:
return Post.objects.get(pk = pk)
except Post.DoesNotExist:
return None
def get(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
serializer = PostSerializer(post)
return Response(serializer.data, status = status.HTTP_200_OK)
def put(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
data = {
'user': request.user.id,
'title': request.data.get('title'),
'body': request.data.get('body'),
'upvote_count': post.upvote_count
}
serializer = PostSerializer(post, data = data, partial = True)
if serializer.is_valid():
if post.user.id == request.user.id:
serializer.save()
return Response(serializer.data, status = status.HTTP_200_OK)
return Response({"error": "You are not authorized to edit this post"}, status = status.HTTP_401_UNAUTHORIZED)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
if post.user.id == request.user.id:
post.delete()
return Response({"res": "Object deleted!"}, status = status.HTTP_200_OK)
return Response({"error": "You are not authorized to delete this post"}, status = status.HTTP_401_UNAUTHORIZED)
The get_object
method is a helper method that retrieves the post with the specified primary key (pk
). If the post does not exist, it returns None
.
The get
method of the PostDetailAPIView
class is called when a GET
request is sent to the view. It uses the get_object
method to retrieve the post with the specified pk
, serializes the post using the PostSerializer
class, and returns the serialized data in an HTTP response with a status code of 200 OK. If the post with the specified pk
does not exist, it returns an error message with a status code of 404 NOT FOUND.
The put
method of the PostDetailAPIView
class is called when a PUT
request is sent to the view. It uses the get_object
method to retrieve the post with the specified pk
, updates the post with the data from the request and saves the updated post. If the current user is not the owner of the post, it returns an error message with a status code of 401 UNAUTHORIZED. If the data from the request is not valid, it returns an error message with a status code of 400 BAD REQUEST.
The delete
method of the PostDetailAPIView
class is called when a DELETE
request is sent to the view. It uses the get_object
method to retrieve the post with the specified pk
and then deletes it if the current user is the owner of the post. If the current user is not the owner of the post, it returns an error message with a status code of 401 UNAUTHORIZED. If the post with the specified pk
does not exist, it returns an error message with a status code of 404 NOT FOUND.
Next, we will be having UserPostAPIView
class which deals with viewing posts from a particular user.
class UserPostAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]def get(self, request, username, *args, **kwargs):
user = User.objects.filter(username = username).first()
if user is None:
return Response({'error': 'User not found'}, status = status.HTTP_404_NOT_FOUND)
posts = Post.objects.filter(user = user)
serializer = PostSerializer(posts, many = True)
return Response(serializer.data, status = status.HTTP_200_OK)
The get
method of the UserPostAPIView
class retrieves the user with the specified username
, retrieves all the posts created by that user, and then creates a serialized representation of the data using the PostSerializer
class. It then returns this data in an HTTP response with a status code of 200 OK. If the user with the specified username
does not exist, it returns an error message with a status code of 404 NOT FOUND.
Next, we will be having the UpvoteAPIView
class which will deal with all the functionalities related to the upvotes part.
class UpvoteAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]def get_object(self, pk):
try:
return Post.objects.get(pk = pk)
except Post.DoesNotExist:
return None
def post(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
upvoters = post.upvotes.all().values_list('user', flat = True)
if request.user.id in upvoters:
post.upvote_count -= 1
post.upvotes.filter(user = request.user).delete()
else:
post.upvote_count += 1
upvote = Upvote(user = request.user, post = post)
upvote.save()
post.save()
serializer = PostSerializer(post)
return Response(serializer.data, status = status.HTTP_200_OK)
The get_object
method is a helper method that retrieves the post with the specified primary key (pk
). If the post does not exist, it returns None
. It is similar to the one we wrote for PostDetailAPIView
class.
The post
method of the UpvoteAPIView
class uses the get_object
method to retrieve the post with the specified pk
and then either adds or removes an upvote for the post by the current user. It then updates the upvote_count
of the post and saves the updated post. Finally, it creates a serialized representation of the post using the PostSerializer
class and returns the serialized data in an HTTP response with a status code of 200 OK. If the post with the specified pk
does not exist, it returns an error message with a status code of 404 NOT FOUND.
Finally, we have the CommentAPIView
which will deal with the comments made on a particular post. It will have the functionality to fetch all the comments for a particular post and to post a new comment.
class CommentAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]def get_object(self, pk):
try:
return Post.objects.get(pk = pk)
except Post.DoesNotExist:
return None
def get(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
comments = Comment.objects.filter(post = post)
serializer = CommentSerializer(comments, many = True)
return Response(serializer.data, status = status.HTTP_200_OK)
def post(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
data = {
'user': request.user.id,
'post': post.id,
'body': request.data.get('body')
}
serializer = CommentSerializer(data = data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status = status.HTTP_201_CREATED)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
As we can see, it has the same get_object
helper function, which takes in the primary key pk
and returns the post corresponding to it.
The get
method of the CommentAPIView
class is called when a GET
request is sent to the view. It uses the get_object
method to retrieve the post with the specified pk
, retrieves all the comments on that post, and then creates a serialized representation of the data using the CommentSerializer
class. It then returns this data in an HTTP response with a status code of 200 OK. If the post with the specified pk
does not exist, it returns an error message with a status code of 404 NOT FOUND.
The post
method of the CommentAPIView
class is called when a POST
request is sent to the view. It uses the get_object
method to retrieve the post with the specified pk
, creates a new Comment
object using the data from the request, serializes the data using the CommentSerializer
class, and returns the serialized data in an HTTP response with a status code of 201 CREATED. If the data from the request is not valid, it returns an error message with a status code of 400 BAD REQUEST. If the post with the specified pk
does not exist, it returns an error message with a status code of 404 NOT FOUND.
The complete code for the views.py
file can be found below.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework import permissions
from .models import Post, Upvote, Comment
from .serializers import PostSerializer, UpvoteSerializer, CommentSerializer
from django.contrib.auth.models import Userclass PostListAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request, *args, **kwargs):
posts = Post.objects.all()
serializer = PostSerializer(posts, many = True)
return Response(serializer.data, status = status.HTTP_200_OK)
def post(self, request, *args, **kwargs):
data = {
'user': request.user.id,
'title': request.data.get('title'),
'body': request.data.get('body')
}
serializer = PostSerializer(data = data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status = status.HTTP_201_CREATED)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
class PostDetailAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get_object(self, pk):
try:
return Post.objects.get(pk = pk)
except Post.DoesNotExist:
return None
def get(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
serializer = PostSerializer(post)
return Response(serializer.data, status = status.HTTP_200_OK)
def put(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
data = {
'user': request.user.id,
'title': request.data.get('title'),
'body': request.data.get('body'),
'upvote_count': post.upvote_count
}
serializer = PostSerializer(post, data = data, partial = True)
if serializer.is_valid():
if post.user.id == request.user.id:
serializer.save()
return Response(serializer.data, status = status.HTTP_200_OK)
return Response({"error": "You are not authorized to edit this post"}, status = status.HTTP_401_UNAUTHORIZED)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
if post.user.id == request.user.id:
post.delete()
return Response({"res": "Object deleted!"}, status = status.HTTP_200_OK)
return Response({"error": "You are not authorized to delete this post"}, status = status.HTTP_401_UNAUTHORIZED)
class UserPostAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request, username, *args, **kwargs):
user = User.objects.filter(username = username).first()
if user is None:
return Response({'error': 'User not found'}, status = status.HTTP_404_NOT_FOUND)
posts = Post.objects.filter(user = user)
serializer = PostSerializer(posts, many = True)
return Response(serializer.data, status = status.HTTP_200_OK)
class UpvoteAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get_object(self, pk):
try:
return Post.objects.get(pk = pk)
except Post.DoesNotExist:
return None
def post(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
upvoters = post.upvotes.all().values_list('user', flat = True)
if request.user.id in upvoters:
post.upvote_count -= 1
post.upvotes.filter(user = request.user).delete()
else:
post.upvote_count += 1
upvote = Upvote(user = request.user, post = post)
upvote.save()
post.save()
serializer = PostSerializer(post)
return Response(serializer.data, status = status.HTTP_200_OK)
class CommentAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get_object(self, pk):
try:
return Post.objects.get(pk = pk)
except Post.DoesNotExist:
return None
def get(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
comments = Comment.objects.filter(post = post)
serializer = CommentSerializer(comments, many = True)
return Response(serializer.data, status = status.HTTP_200_OK)
def post(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
data = {
'user': request.user.id,
'post': post.id,
'body': request.data.get('body')
}
serializer = CommentSerializer(data = data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status = status.HTTP_201_CREATED)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
Next, we move to admin.py
file to register the models we created so that the admin panel has the access to those models.
admin.py
from django.contrib import admin
from .models import Post, Upvote, Commentadmin.site.register(Post)
admin.site.register(Upvote)
admin.site.register(Comment)
As shown above, we registered the three models — Post
, Upvote
and Comment
.
Next, we will create a file named urls.py
in our posts
folder, and we will write the URLs for all the views we wrote for our posts
app.
urls.py
In the urls.py
file, we will be defining the URL patterns for our posts
app, and then we will have to include them in the urls.py
file present in the blog
application.
from django.urls import path
from .views import PostListAPIView, PostDetailAPIView, UserPostAPIView, UpvoteAPIView, CommentAPIViewurlpatterns = [
path('', PostListAPIView.as_view()),
path('<int:pk>/', PostDetailAPIView.as_view()),
path('<int:pk>/upvote/', UpvoteAPIView.as_view()),
path('<int:pk>/comment/', CommentAPIView.as_view()),
path('<username>/', UserPostAPIView.as_view())
]
Here, urlpatterns
is a list of URL patterns for the blog app. Each element in the list is a path
object that maps a URL path to a view.
The first element in the list maps the root URL (/
) to the PostListAPIView
view, so when a user accesses the root URL, the PostListAPIView
view will be called and handle the request. The other elements in the list map URLs of the form /<int:pk>/
, /<int:pk>/upvote/
, /<int:pk>/comment/
, and /<username>/
to the corresponding views. These URLs are used to access individual posts, upvote/downvote posts, create comments on posts, and view the posts of a specific user.
<int:pk>
is a URL path parameter that specifies that the URL path should contain an integer value (the primary key of the post) at that position. This integer value is passed as the pk
argument to the view when the URL is accessed, and the view can use it to retrieve the post with the specified primary key. This allows the view to handle requests for specific posts rather than all posts.
Similarly, /<username>/
is used to access posts from a particular user. The username
is passed to the view when the URL is accessed.
Now, in the blog
application’s urls.py
file, we will include the URLs of the posts
app as we did for the users
app.
from django.contrib import admin
from django.urls import path, include
from posts import urls as posts_urls
from users import urls as users_urlsurlpatterns = [
path("admin/", admin.site.urls),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
path("api/posts/", include(posts_urls)),
path("api/users/", include(users_urls)),
]
Before we can test the APIs, we need to perform migrations since we have created new models.
So, we will run the following command to do so:
python manage.py makemigrations
python manage.py migrate
We need to run a migration when we create a new model in Django because the database schema needs to match the structure of the data that will be stored in it. Without running a migration, the database will not have the correct tables and fields to store the data from the new model, and we will get errors when trying to save or retrieve data from the model.
The python manage.py makemigrations
command analyzes the changes we have made to our models and creates a new migration file that contains the instructions for updating the database schema.
The python manage.py migrate
command applies the changes in the migration file to the database. This updates the database schema and ensures it is in sync with our models.
We can test the APIs for the posts
app in the same way as we did for the users
app. Since this article has already been too big, so I will leave the testing part for the posts
app for you to do yourself. It will be very much the same way you did for the users
app by first running the server and then going to the respective URLs as defined in the urls.py
file.
So, that brings us to the end of the backend part of this article series. I hope you all enjoyed the backend part and learnt how DRF works, and now you should try to build it on your own and add functionalities to it to enhance the application further.
I will soon start the work on the frontend of our website and will be adding the articles on the frontend part soon. I will link them up here when they are ready. Till then, keep learning!
Here are some more article series you would love to read:
In the third part, we deal with the entire posts app of our application, thus finishing the backend of our application
Hello folks, I hope you all are doing well and that you enjoyed the first two parts of this DRF article series. In the first part of the series, we dealt with the basics of setting up this project, and you all got an overview of the project we will build, while in the second part, we dealt with the users
app of our application where we wrote the serializer and view for our users
part
If you have not yet read those parts, I strongly suggest you go and first read the first two parts and then come back to this article.
In the third part of the series, we will deal with the complete posts
app of our Django application, thus finishing up the complete backend for our application. We would also be testing the APIs using the Browsable API interface in the same way we did for the users
app. So, let us dive right into the posts
app and start building it.
So, when we move into the posts
folder, we will see we have the same files as we did for the users
app when we moved into that folder. First, we would be moving into the models.py
file to build our models.
models.py
Let’s start with the models.py
file first. We will define our database models here. So, we will have three different models for the posts
part — Post
, Upvote
and Comment
.
We will first start with importing the required dependencies into our file, as shown below:
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
As we can see, we have imported the default User
model and also imported models
and reverse
from Django. These would help us in building our models.
So, we would start with the Posts
model. Let us first look into the code, and then we will understand the significance of those lines.
class Post(models.Model):
user = models.ForeignKey(User, on_delete = models.CASCADE)
title = models.CharField(max_length = 100)
body = models.TextField()
created = models.DateTimeField(auto_now_add = True)
updated = models.DateTimeField(auto_now = True)
upvote_count = models.IntegerField(default = 0)def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post-detail', kwargs = {'pk': self.pk})
The code above defines a Post
class that inherits from Django’s models.Model
class. This makes the Post
class a Django model, which allows it to be saved in and retrieved from a database.
The Post
class has several fields, including user
, title
, body
, created
, updated
, and upvote_count
. Each field is defined using a class from Django’s models
module, which specifies the type of data that each field will store. For example, the title
field is a CharField
, which will store the title of the blog post, and the created
field is a DateTimeField
, which stores the time at which the post was created.
The user
field is defined as a ForeignKey
field, which means that it is a reference to the default Django User
model. The on_delete
argument specifies what should happen to a Post
object if the User
object that it references is deleted. Here, the on_delete
argument is set to models.CASCADE
, which signifies that if the User
object is deleted, the Post
object will also be deleted.
The ForeignKey
field allows a Post
to be linked to a specific User
object, which can be accessed using the post.user
attribute. This can be used to get the username of the user who created the Post
, or to allow only the user who created the Post
to edit or delete it.
The upvote_count
field will store the number of upvotes the blog post gets from the users. The default value has been set to zero, which signifies that when a new Post
object is created, the upvote_count
is set to zero. The upvote_count
will be incremented when the user clicks on the upvote button on the blog post.
The __str__
method is a special method that defines how a Post
object should be represented as a string. In this case, the method returns the title
of the Post
.
The get_absolute_url
method returns the URL of the Post
object’s detail page. Django uses this to determine the URL of the object when it is saved to the database.
Next, we would look into the Upvote
model that will store all the upvotes in our application. Let us have a look at the model, and then I will explain the significance of the code.
class Upvote(models.Model):
user = models.ForeignKey(User, related_name = 'upvotes', on_delete = models.CASCADE)
post = models.ForeignKey(Post, related_name = 'upvotes', on_delete = models.CASCADE)
This code defines an Upvote
class that inherits from Django’s models.Model
class, just like the Post
class that we discussed earlier.
The Upvote
class has two fields: user
and post
. Both fields have been defined as ForeignKey
fields, which signifies that they are references to User
and Post
objects, respectively. The related_name
argument specifies the name that should be used to access the reverse relationship. For example, if a User
object user
has upvoted several Post
objects, the user.upvotes
attribute will return a queryset of the Post
objects that user
has upvoted.
The on_delete
argument for both fields is set to models.CASCADE
, which means that if either the User
or the Post
object that an Upvote
object references is deleted, the Upvote
object will also be deleted.
This class is used to track which users have upvoted which posts. We will use this to prevent a user from upvoting a Post
more than once, or we can also use this to retrieve the list of users who have upvoted a given Post
, though we will not be implementing the latter in our application. But, as we have seen, it is easy to do so, and you can surely try to implement it to improve the application.
Next, we move to the final model class Comment
, which will store all the comments made on the blog posts in our application. The code for the same is below.
class Comment(models.Model):
user = models.ForeignKey(User, related_name = 'comments', on_delete = models.CASCADE)
post = models.ForeignKey(Post, related_name = 'comments', on_delete = models.CASCADE)
body = models.TextField()
created = models.DateTimeField(auto_now_add = True)def __str__(self):
return self.body
The code defines a Comment
class that inherits from Django’s models.Model
class, just like the Post
and Upvote
classes.
The Comment
class has four fields: user
, post
, body
and created
. The user
and post
fields are defined as ForeignKey
fields, which means that they are references to User
and Post
objects, respectively. The related_name
argument for both fields specifies the name that should be used to access the reverse relationship. For example, if a User
object user
has written several Comment
objects, the user.comments
attribute will return a queryset of the Comment
objects that user
has written.
The body
field is a TextField
, which stores the main content of the comment is stored.
The created
field is a DateTimeField
that stores the date and time when the Comment
object was created. The auto_now_add
argument is set to True
, which means that the Comment
object’s created
field will automatically be set to the current date and time when the object is first created.
The __str__
method is a special method that defines how a Comment
object should be represented as a string. In this case, the method returns the body
of the Comment
, which is the main content of the comment.
The Comment
class is used to determine which users have commented on a particular Post
object. We will use this to display the comments on a blog post along with the user’s name. Also, this can be used to allow only the user who created the comment to edit or delete it though we have not implemented this part in the current application.
The complete code for the models.py
section can be found below:
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverseclass Post(models.Model):
user = models.ForeignKey(User, on_delete = models.CASCADE)
title = models.CharField(max_length = 100)
body = models.TextField()
created = models.DateTimeField(auto_now_add = True)
updated = models.DateTimeField(auto_now = True)
upvote_count = models.IntegerField(default = 0)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post-detail', kwargs = {'pk': self.pk})
class Upvote(models.Model):
user = models.ForeignKey(User, related_name = 'upvotes', on_delete = models.CASCADE)
post = models.ForeignKey(Post, related_name = 'upvotes', on_delete = models.CASCADE)
class Comment(models.Model):
user = models.ForeignKey(User, related_name = 'comments', on_delete = models.CASCADE)
post = models.ForeignKey(Post, related_name = 'comments', on_delete = models.CASCADE)
body = models.TextField()
created = models.DateTimeField(auto_now_add = True)
def __str__(self):
return self.body
Next, we would be moving to serializers.py
file. Since this file would not be present by default, we need to create it as we did for the users
app.
serializers.py
In this file, we will serialize our models to make them in a format which can be shared across the web and can be accessed by any frontend framework.
First, we will be importing the required dependencies in our file. We will import serializers
from rest_framework
package, and also we import the three models we created — Post
, Upvote
and Comment
.
Next, we will write three serializers — one for each of the three models. The code for the serializers class is given below. Let us first look at the code, and then we can discuss it later.
from rest_framework import serializers
from .models import Post, Upvote, Commentclass PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ('title', 'body', 'created', 'updated', 'user', 'upvote_count')
class UpvoteSerializer(serializers.ModelSerializer):
class Meta:
model = Upvote
fields = ('user', 'post')
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ('user', 'post', 'body', 'created')
The structure is very similar to the serializer we wrote for the Users
model in the previous article.
Each of the three classes written above defines a serializer for a specific model: Post
, Upvote
, and Comment
. The Meta
inner class of each serializer class specifies the model that the serializer should work with and which fields of the model should be included in the serialized data.
As we can see, PostSerializer
will serialize instances of the Post
model and will include the title
, body
, created
, updated
, user
, and upvote_count
fields in the serialized data.
Similarly, UpvoteSerializer
and CommentSerializer
will serialize their own fields.
Next, we move to the most important part — views, where we would be defining all the logic of the application, and we will deal with get
, post
, put
and delete
requests in that file.
views.py
In this section, we will define various views
for handling different requests related to the three models, we have created.
We will be adding the following functionalities to our application:
- Seeing all the blog posts
- Creating a new blog post
- Editing a blog post written by that particular user
- Deleting a blog post written by that particular user
- Seeing the blog posts written by a specific user
- Upvoting the post
- Adding a comment on the post
So, let us start with the required imports.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework import permissions
from .models import Post, Upvote, Comment
from .serializers import PostSerializer, UpvoteSerializer, CommentSerializer
from django.contrib.auth.models import User
So, in the views.py
file, we have imported various classes and functions — some from rest_framework
package, while some are the serilializers
and models
.
APIView
is a class that provides a way to define views that handle incoming HTTP requests in a RESTful manner. A view is a callable that takes an HTTP request as its argument and returns an HTTP response.
Response
is a class that provides a way to create HTTP response objects. An HTTP response typically includes a status code, headers, and a body containing the response data.
status
is a module that defines a number of HTTP status codes as constants. These codes indicate the success or failure of an HTTP request.
permissions
is a module that provides classes for implementing permission checks in views. A permission is a rule that determines whether a user has the right to access a particular view.
The code also imports several model classes from the .models
module, as well as the serializer classes from the .serializers
module. Finally, it imports the User
class from the django.contrib.auth.models
module, which is a class that represents a user of the Django web framework, i.e. the default Django User
model class.
Also, all the classes in the views.py
file, we will require the user to be logged in, so we will use the permissions
module to enforce this in all the classes.
Next, we will define PostListAPIView
class which will provide the ability to fetch all the blog posts available and also will have the ability to create a new blog post. Here is the code for the same:
class PostListAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]def get(self, request, *args, **kwargs):
posts = Post.objects.all()
serializer = PostSerializer(posts, many = True)
return Response(serializer.data, status = status.HTTP_200_OK)
def post(self, request, *args, **kwargs):
data = {
'user': request.user.id,
'title': request.data.get('title'),
'body': request.data.get('body')
}
serializer = PostSerializer(data = data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status = status.HTTP_201_CREATED)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
This is a subclass of APIView
, which provides several methods for handling different HTTP request methods, such as GET
, POST
, PUT
, and DELETE
.
The permission_classes
attribute of the PostListAPIView
class specifies that only authenticated users are allowed to access this view. This is achieved by using the isAuthenticated
method. This means that only users who have logged in will be able to see the list of posts or create new posts. We will be using the same in all other classes too.
The get
method of the PostListAPIView
class is called when a GET
request is sent to the view. This method retrieves all the blog posts on the website and creates a serialized representation of the data using the PostSerializer
class. It then returns this data in an HTTP response with a status code of 200 OK.
The post
method of the PostListAPIView
class is called when a POST
request is sent to the view. This method creates a new Post
object using the data from the request, serializes the data using the PostSerializer
class, and returns the serialized data in an HTTP response with a status code of 201 CREATED. If the data from the request is not valid, it returns an error message with a status code of 400 BAD REQUEST.
Next, we define the PostDetailAPIView
which will deal with the operations of an individual post in a blog. It will handle fetching that particular post, editing it or deleting it. It also has the same permissions which we had for the previous class. Here is the code for it:
class PostDetailAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]def get_object(self, pk):
try:
return Post.objects.get(pk = pk)
except Post.DoesNotExist:
return None
def get(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
serializer = PostSerializer(post)
return Response(serializer.data, status = status.HTTP_200_OK)
def put(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
data = {
'user': request.user.id,
'title': request.data.get('title'),
'body': request.data.get('body'),
'upvote_count': post.upvote_count
}
serializer = PostSerializer(post, data = data, partial = True)
if serializer.is_valid():
if post.user.id == request.user.id:
serializer.save()
return Response(serializer.data, status = status.HTTP_200_OK)
return Response({"error": "You are not authorized to edit this post"}, status = status.HTTP_401_UNAUTHORIZED)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
if post.user.id == request.user.id:
post.delete()
return Response({"res": "Object deleted!"}, status = status.HTTP_200_OK)
return Response({"error": "You are not authorized to delete this post"}, status = status.HTTP_401_UNAUTHORIZED)
The get_object
method is a helper method that retrieves the post with the specified primary key (pk
). If the post does not exist, it returns None
.
The get
method of the PostDetailAPIView
class is called when a GET
request is sent to the view. It uses the get_object
method to retrieve the post with the specified pk
, serializes the post using the PostSerializer
class, and returns the serialized data in an HTTP response with a status code of 200 OK. If the post with the specified pk
does not exist, it returns an error message with a status code of 404 NOT FOUND.
The put
method of the PostDetailAPIView
class is called when a PUT
request is sent to the view. It uses the get_object
method to retrieve the post with the specified pk
, updates the post with the data from the request and saves the updated post. If the current user is not the owner of the post, it returns an error message with a status code of 401 UNAUTHORIZED. If the data from the request is not valid, it returns an error message with a status code of 400 BAD REQUEST.
The delete
method of the PostDetailAPIView
class is called when a DELETE
request is sent to the view. It uses the get_object
method to retrieve the post with the specified pk
and then deletes it if the current user is the owner of the post. If the current user is not the owner of the post, it returns an error message with a status code of 401 UNAUTHORIZED. If the post with the specified pk
does not exist, it returns an error message with a status code of 404 NOT FOUND.
Next, we will be having UserPostAPIView
class which deals with viewing posts from a particular user.
class UserPostAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]def get(self, request, username, *args, **kwargs):
user = User.objects.filter(username = username).first()
if user is None:
return Response({'error': 'User not found'}, status = status.HTTP_404_NOT_FOUND)
posts = Post.objects.filter(user = user)
serializer = PostSerializer(posts, many = True)
return Response(serializer.data, status = status.HTTP_200_OK)
The get
method of the UserPostAPIView
class retrieves the user with the specified username
, retrieves all the posts created by that user, and then creates a serialized representation of the data using the PostSerializer
class. It then returns this data in an HTTP response with a status code of 200 OK. If the user with the specified username
does not exist, it returns an error message with a status code of 404 NOT FOUND.
Next, we will be having the UpvoteAPIView
class which will deal with all the functionalities related to the upvotes part.
class UpvoteAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]def get_object(self, pk):
try:
return Post.objects.get(pk = pk)
except Post.DoesNotExist:
return None
def post(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
upvoters = post.upvotes.all().values_list('user', flat = True)
if request.user.id in upvoters:
post.upvote_count -= 1
post.upvotes.filter(user = request.user).delete()
else:
post.upvote_count += 1
upvote = Upvote(user = request.user, post = post)
upvote.save()
post.save()
serializer = PostSerializer(post)
return Response(serializer.data, status = status.HTTP_200_OK)
The get_object
method is a helper method that retrieves the post with the specified primary key (pk
). If the post does not exist, it returns None
. It is similar to the one we wrote for PostDetailAPIView
class.
The post
method of the UpvoteAPIView
class uses the get_object
method to retrieve the post with the specified pk
and then either adds or removes an upvote for the post by the current user. It then updates the upvote_count
of the post and saves the updated post. Finally, it creates a serialized representation of the post using the PostSerializer
class and returns the serialized data in an HTTP response with a status code of 200 OK. If the post with the specified pk
does not exist, it returns an error message with a status code of 404 NOT FOUND.
Finally, we have the CommentAPIView
which will deal with the comments made on a particular post. It will have the functionality to fetch all the comments for a particular post and to post a new comment.
class CommentAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]def get_object(self, pk):
try:
return Post.objects.get(pk = pk)
except Post.DoesNotExist:
return None
def get(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
comments = Comment.objects.filter(post = post)
serializer = CommentSerializer(comments, many = True)
return Response(serializer.data, status = status.HTTP_200_OK)
def post(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
data = {
'user': request.user.id,
'post': post.id,
'body': request.data.get('body')
}
serializer = CommentSerializer(data = data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status = status.HTTP_201_CREATED)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
As we can see, it has the same get_object
helper function, which takes in the primary key pk
and returns the post corresponding to it.
The get
method of the CommentAPIView
class is called when a GET
request is sent to the view. It uses the get_object
method to retrieve the post with the specified pk
, retrieves all the comments on that post, and then creates a serialized representation of the data using the CommentSerializer
class. It then returns this data in an HTTP response with a status code of 200 OK. If the post with the specified pk
does not exist, it returns an error message with a status code of 404 NOT FOUND.
The post
method of the CommentAPIView
class is called when a POST
request is sent to the view. It uses the get_object
method to retrieve the post with the specified pk
, creates a new Comment
object using the data from the request, serializes the data using the CommentSerializer
class, and returns the serialized data in an HTTP response with a status code of 201 CREATED. If the data from the request is not valid, it returns an error message with a status code of 400 BAD REQUEST. If the post with the specified pk
does not exist, it returns an error message with a status code of 404 NOT FOUND.
The complete code for the views.py
file can be found below.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework import permissions
from .models import Post, Upvote, Comment
from .serializers import PostSerializer, UpvoteSerializer, CommentSerializer
from django.contrib.auth.models import Userclass PostListAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request, *args, **kwargs):
posts = Post.objects.all()
serializer = PostSerializer(posts, many = True)
return Response(serializer.data, status = status.HTTP_200_OK)
def post(self, request, *args, **kwargs):
data = {
'user': request.user.id,
'title': request.data.get('title'),
'body': request.data.get('body')
}
serializer = PostSerializer(data = data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status = status.HTTP_201_CREATED)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
class PostDetailAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get_object(self, pk):
try:
return Post.objects.get(pk = pk)
except Post.DoesNotExist:
return None
def get(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
serializer = PostSerializer(post)
return Response(serializer.data, status = status.HTTP_200_OK)
def put(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
data = {
'user': request.user.id,
'title': request.data.get('title'),
'body': request.data.get('body'),
'upvote_count': post.upvote_count
}
serializer = PostSerializer(post, data = data, partial = True)
if serializer.is_valid():
if post.user.id == request.user.id:
serializer.save()
return Response(serializer.data, status = status.HTTP_200_OK)
return Response({"error": "You are not authorized to edit this post"}, status = status.HTTP_401_UNAUTHORIZED)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
if post.user.id == request.user.id:
post.delete()
return Response({"res": "Object deleted!"}, status = status.HTTP_200_OK)
return Response({"error": "You are not authorized to delete this post"}, status = status.HTTP_401_UNAUTHORIZED)
class UserPostAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request, username, *args, **kwargs):
user = User.objects.filter(username = username).first()
if user is None:
return Response({'error': 'User not found'}, status = status.HTTP_404_NOT_FOUND)
posts = Post.objects.filter(user = user)
serializer = PostSerializer(posts, many = True)
return Response(serializer.data, status = status.HTTP_200_OK)
class UpvoteAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get_object(self, pk):
try:
return Post.objects.get(pk = pk)
except Post.DoesNotExist:
return None
def post(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
upvoters = post.upvotes.all().values_list('user', flat = True)
if request.user.id in upvoters:
post.upvote_count -= 1
post.upvotes.filter(user = request.user).delete()
else:
post.upvote_count += 1
upvote = Upvote(user = request.user, post = post)
upvote.save()
post.save()
serializer = PostSerializer(post)
return Response(serializer.data, status = status.HTTP_200_OK)
class CommentAPIView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get_object(self, pk):
try:
return Post.objects.get(pk = pk)
except Post.DoesNotExist:
return None
def get(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
comments = Comment.objects.filter(post = post)
serializer = CommentSerializer(comments, many = True)
return Response(serializer.data, status = status.HTTP_200_OK)
def post(self, request, pk, *args, **kwargs):
post = self.get_object(pk)
if post is None:
return Response({'error': 'Post not found'}, status = status.HTTP_404_NOT_FOUND)
data = {
'user': request.user.id,
'post': post.id,
'body': request.data.get('body')
}
serializer = CommentSerializer(data = data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status = status.HTTP_201_CREATED)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
Next, we move to admin.py
file to register the models we created so that the admin panel has the access to those models.
admin.py
from django.contrib import admin
from .models import Post, Upvote, Commentadmin.site.register(Post)
admin.site.register(Upvote)
admin.site.register(Comment)
As shown above, we registered the three models — Post
, Upvote
and Comment
.
Next, we will create a file named urls.py
in our posts
folder, and we will write the URLs for all the views we wrote for our posts
app.
urls.py
In the urls.py
file, we will be defining the URL patterns for our posts
app, and then we will have to include them in the urls.py
file present in the blog
application.
from django.urls import path
from .views import PostListAPIView, PostDetailAPIView, UserPostAPIView, UpvoteAPIView, CommentAPIViewurlpatterns = [
path('', PostListAPIView.as_view()),
path('<int:pk>/', PostDetailAPIView.as_view()),
path('<int:pk>/upvote/', UpvoteAPIView.as_view()),
path('<int:pk>/comment/', CommentAPIView.as_view()),
path('<username>/', UserPostAPIView.as_view())
]
Here, urlpatterns
is a list of URL patterns for the blog app. Each element in the list is a path
object that maps a URL path to a view.
The first element in the list maps the root URL (/
) to the PostListAPIView
view, so when a user accesses the root URL, the PostListAPIView
view will be called and handle the request. The other elements in the list map URLs of the form /<int:pk>/
, /<int:pk>/upvote/
, /<int:pk>/comment/
, and /<username>/
to the corresponding views. These URLs are used to access individual posts, upvote/downvote posts, create comments on posts, and view the posts of a specific user.
<int:pk>
is a URL path parameter that specifies that the URL path should contain an integer value (the primary key of the post) at that position. This integer value is passed as the pk
argument to the view when the URL is accessed, and the view can use it to retrieve the post with the specified primary key. This allows the view to handle requests for specific posts rather than all posts.
Similarly, /<username>/
is used to access posts from a particular user. The username
is passed to the view when the URL is accessed.
Now, in the blog
application’s urls.py
file, we will include the URLs of the posts
app as we did for the users
app.
from django.contrib import admin
from django.urls import path, include
from posts import urls as posts_urls
from users import urls as users_urlsurlpatterns = [
path("admin/", admin.site.urls),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
path("api/posts/", include(posts_urls)),
path("api/users/", include(users_urls)),
]
Before we can test the APIs, we need to perform migrations since we have created new models.
So, we will run the following command to do so:
python manage.py makemigrations
python manage.py migrate
We need to run a migration when we create a new model in Django because the database schema needs to match the structure of the data that will be stored in it. Without running a migration, the database will not have the correct tables and fields to store the data from the new model, and we will get errors when trying to save or retrieve data from the model.
The python manage.py makemigrations
command analyzes the changes we have made to our models and creates a new migration file that contains the instructions for updating the database schema.
The python manage.py migrate
command applies the changes in the migration file to the database. This updates the database schema and ensures it is in sync with our models.
We can test the APIs for the posts
app in the same way as we did for the users
app. Since this article has already been too big, so I will leave the testing part for the posts
app for you to do yourself. It will be very much the same way you did for the users
app by first running the server and then going to the respective URLs as defined in the urls.py
file.
So, that brings us to the end of the backend part of this article series. I hope you all enjoyed the backend part and learnt how DRF works, and now you should try to build it on your own and add functionalities to it to enhance the application further.
I will soon start the work on the frontend of our website and will be adding the articles on the frontend part soon. I will link them up here when they are ready. Till then, keep learning!
Here are some more article series you would love to read: