Building a Social Media Site With Python and Django: Part 4 Edit/Delete Posts, Add Comments, Restricted Views
In this tutorial we are going to finish our post and comment functionality. First, we need to add the ability to edit and delete posts. We also need to make sure to only allow this to the user who originally created the post. After that, we will need to add the ability to add comments and delete comments. We also need to restrict the edit and delete views to be only accessible by the user who created the post/comment. Let’s get started by adding the edit and delete views for the posts.
Editing and Deleting Posts
First, let’s add an edit view and delete view to our views.py:
from django.views.generic.edit import UpdateView, DeleteViewclass PostEditView(UpdateView):
model = Post
fields = ['body']
template_name = 'social/post_edit.html'
def get_success_url(self):
pk = self.kwargs['pk']
return reverse_lazy('post-detail', kwargs={'pk': pk})class PostDeleteView(DeleteView):
model = Post
template_name = 'social/post_delete.html'
success_url = reverse_lazy('post-list')
We will use the generic UpdateView and DeleteView to make this easier. We can just pass in the model we want to use, the template name, and where to redirect on success and the generic view will handle everything else.
Now with that done, let’s create teh edit template and the delete template. Let’s start with the edit template:
{% extends 'landing/base.html' %}
{% load crispy_forms_tags %}{% block content %}
<div class="container">
<div class="row mt-5">
<div class="col-md-3 col-sm-6">
<a href="{% url 'post-detail' object.pk %}" class="btn btn-light">Back to Feed</a>
</div>
</div>
<div class="row justify-content-center mt-5">
<div class="col-md-5 col-sm-12">
<h5>Update Your Post</h5>
</div>
</div>
<div class="row justify-content-center mt-3 mb-5">
<div class="col-md-5 col-sm-12">
<form method="POST">
{% csrf_token %}
{{ form | crispy }}
<div class="d-grid gap-2">
<button class="btn btn-success mt-3">Submit!</button>
</div>
</form>
</div>
</div>
</div>
{% endblock content %}
This will just contain a form to update the selected post. With the generic view, we have ‘form’ as a variable to access the form. Now let’s create the delete template, this will just be a confirmation template:
{% extends 'landing/base.html' %}
{% load crispy_forms_tags %}{% block content %}
<div class="container">
<div class="row mt-5">
<div class="col-md-3 col-sm-6">
<a href="{% url 'post-detail' object.pk %}" class="btn btn-light">Back to Feed</a>
</div>
</div>
<div class="row justify-content-center mt-5">
<div class="col-md-5 col-sm-12">
<h5>Are You Sure?</h5>
<p>You are about to delete this post, this cannot be undone.</p>
<form method="POST">
{% csrf_token %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-danger">Delete</button>
</div>
</form>
</div>
</div>
</div>
{% endblock content %}
Now let’s create the url patterns. We will put this in social/urls.py:
from django.urls import path
from .views import PostListView, PostDetailView, PostEditView, PostDeleteViewurlpatterns = [
path('', PostListView.as_view(), name='post-list'),
path('post/<int:pk>/', PostDetailView.as_view(), name='post-detail'),
path('post/edit/<int:pk>/', PostEditView.as_view(), name='post-edit'),
path('post/delete/<int:pk>/', PostDeleteView.as_view(), name='post-delete'),
]
Now let’s add some buttons on our post_detail page to access these views:
<div class="row justify-content-center mt-3">
<div class="col-md-5 col-sm-12 border-bottom">
<p>
<strong>{{ post.author }}</strong> {{ post.created_on }}
{% if request.user == post.author %}
<a href="{% url 'post-edit' post.pk %}" style="color: #333;"><i class="far fa-edit"></i></a>
<a href="{% url 'post-delete' post.pk %}" style="color: #333;"><i class="fas fa-trash"></i></a>
{% endif %}
</p>
<p>{{ post.body }}</p>
</div>
</div>
Now with that done, we should be able to update and delete posts. Now let’s work on adding comments.
Creating and Deleting Comments
First we need to add to our PostDetailView to handle a post request from our comment form:
class PostDetailView(View):
def get(self, request, pk, *args, **kwargs):
post = Post.objects.get(pk=pk)
form = CommentForm()
comments = Comment.objects.filter(post=post).order_by('-created_on') context = {
'post': post,
'form': form,
'comments': comments,
} return render(request, 'social/post_detail.html', context) def post(self, request, pk, *args, **kwargs):
post = Post.objects.get(pk=pk)
form = CommentForm(request.POST) if form.is_valid():
new_comment = form.save(commit=False)
new_comment.author = request.user
new_comment.post = post
new_comment.save()
comments = Comment.objects.filter(post=post).order_by('-created_on') context = {
'post': post,
'form': form,
'comments': comments,
} return render(request, 'social/post_detail.html', context)
First, we are adding comments to the get method so that we can list them all on the page later. Next we move to the post method, this is where we will handle post requests when the add a comment form is submitted. First we need to get the post and form like last time, but now we are passing in the request.POST data to our form. We then need to check if the form is valid. If it is, we need to save the form, add the author and post based off of the currently logged in user and the current post. We can then save the form as a new comment. From there, we can just filter comments by the current post and add it all to the context before rendering the template.
With that completed, we should be able to add comments, let’s list out all of the comments and add a button to delete comments in the post_detail template:
<div class="row justify-content-center mt-3">
<div class="col-md-5 col-sm-12">
<h5>Add a Comment!</h5>
</div>
</div>
<div class="row justify-content-center mt-3 mb-5">
<div class="col-md-5 col-sm-12">
<form method="POST">
{% csrf_token %}
{{ form | crispy }}
<div class="d-grid gap-2">
<button class="btn btn-success mt-3">Submit!</button>
</div>
</form>
</div>
</div>
{% for comment in comments %}
<div class="row justify-content-center mt-3 mb-5 border-bottom">
<div class="col-md-5 col-sm-12">
<p>
<strong>{{ comment.author }}</strong> {{ comment.created_on }}
{% if request.user == comment.author %}
<a href="{% url 'comment-delete' post.pk comment.pk %}" style="color: #333;"><i class="fas fa-trash"></i></a>
{% endif %}
</p>
<p>{{ comment.comment }}</p>
</div>
</div>
{% endfor %}
</div>
{% endblock content %}
We will just loop through the comments and list them out. We are also checking if the logged in user matches the author on the current comment. If it does, we will add a trash can icon next to it. We will use that next when we add the ability to delete comments.
To begin, we need to create a view for deleting comments:
class PostDeleteView(DeleteView):
model = Post
template_name = 'social/post_delete.html'
success_url = reverse_lazy('post-list')
Now we need to create a confirmation template asking the user if they want to delete the comment. We will call it post_delete.html since that is what we put as the template_name:
{% extends 'landing/base.html' %}
{% load crispy_forms_tags %}{% block content %}
<div class="container">
<div class="row mt-5">
<div class="col-md-3 col-sm-6">
<a href="{% url 'post-detail' object.post.pk %}" class="btn btn-light">Back to Feed</a>
</div>
</div>
<div class="row justify-content-center mt-5">
<div class="col-md-5 col-sm-12">
<h5>Are You Sure?</h5>
<p>You are about to delete this comment, this cannot be undone.</p>
<form method="POST">
{% csrf_token %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-danger">Delete</button>
</div>
</form>
</div>
</div>
</div>
{% endblock content %}
Now let’s add the url for this:
from django.urls import path
from .views import PostListView, PostDetailView, PostEditView, PostDeleteView, CommentDeleteViewurlpatterns = [
path('', PostListView.as_view(), name='post-list'),
path('post/<int:pk>/', PostDetailView.as_view(), name='post-detail'),
path('post/edit/<int:pk>/', PostEditView.as_view(), name='post-edit'),
path('post/delete/<int:pk>/', PostDeleteView.as_view(), name='post-delete'),
path('post/<int:post_pk>/comment/delete/<int:pk>/', CommentDeleteView.as_view(), name='comment-delete'),
]
That should be it, with that done we should be able to delete comments as well. That leaves us with just one more thing to do, which is restricting views.
Restricting Views
We want to only allow logged in users to access the social feed and the post detail page, we also only want the user who created the post or comment originally to be able to edit or delete it. We can restrict these views using some django mixins. Here is the updated views.py file:
from django.shortcuts import render
from django.urls import reverse_lazy
from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin
from django.views import View
from django.views.generic.edit import UpdateView, DeleteView
from .models import Post, Comment
from .forms import PostForm, CommentForm
class PostListView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
posts = Post.objects.all().order_by('-created_on')
form = PostForm() context = {
'post_list': posts,
'form': form,
}
return render(request, 'social/post_list.html', context) def post(self, request, *args, **kwargs):
posts = Post.objects.all().order_by('-created_on')
form = PostForm(request.POST) if form.is_valid():
new_post = form.save(commit=False)
new_post.author = request.user
new_post.save() context = {
'post_list': posts,
'form': form,
}
return render(request, 'social/post_list.html', context)class PostDetailView(LoginRequiredMixin, View):
def get(self, request, pk, *args, **kwargs):
post = Post.objects.get(pk=pk)
form = CommentForm()
comments = Comment.objects.filter(post=post).order_by('-created_on') context = {
'post': post,
'form': form,
'comments': comments,
} return render(request, 'social/post_detail.html', context) def post(self, request, pk, *args, **kwargs):
post = Post.objects.get(pk=pk)
form = CommentForm(request.POST) if form.is_valid():
new_comment = form.save(commit=False)
new_comment.author = request.user
new_comment.post = post
new_comment.save()
comments = Comment.objects.filter(post=post).order_by('-created_on') context = {
'post': post,
'form': form,
'comments': comments,
} return render(request, 'social/post_detail.html', context)class PostEditView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
fields = ['body']
template_name = 'social/post_edit.html'
def get_success_url(self):
pk = self.kwargs['pk']
return reverse_lazy('post-detail', kwargs={'pk': pk})
def test_func(self):
post = self.get_object()
return self.request.user == post.authorclass PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = Post
template_name = 'social/post_delete.html'
success_url = reverse_lazy('post-list') def test_func(self):
post = self.get_object()
return self.request.user == post.authorclass CommentDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = Comment
template_name = 'social/comment_delete.html' def get_success_url(self):
pk = self.kwargs['post_pk']
return reverse_lazy('post-detail', kwargs={'pk': pk}) def test_func(self):
comment = self.get_object()
return self.request.user == comment.author
We can use the LoginRequiredMixin to only make these views accessible to logged in users, if a user goes to one of these pages and is not logged in it will redirect them to the login page. We can also use UserPassesTestMixin to check if they user is allowed to access a page using a boolean expression inside of a method called test_func(). In this case, we are checking if the currently logged in user matches the user on the post or comment that they are trying to edit or delete. If they match, it will let them edit or delete it, if they do not match it will throw a 403 error.
That completes the additions we are adding in this tutorial. We will come back and add more to this application in the future.