Building a Social Media App With Python 3 and Django: Part 12 User Notifications

Video Tutorial

Code on Github

In this tutorial, we are going to add user notifications to our social media app. We will set up notifications that will be in the navigation bar that will list all unseen notifications. For our app, we will count a notification as unseen if they haven’t clicked on it. This will definitely be more involved and not as simple as the previous changes we have made. To make this all work we will mix in a little Javascript and custom Django template tags.

Move the Javascript Script Tag

First let’s fix something that we had from a previous tutorial. We put the Javascript script tag in the post_detail, let’s move that to the base.html

<script type="text/javascript" src="{% static 'js/social.js' %}"></script>

Add Notification Model to Models.py

Now that we have that out of the way, we can start adding notifications. To start, let’s add the Notification model to our social models.py file. In this model, we will add a notification type. This will be a number, either 1, 2, or 3. Each number will correspond to a type, we will use 1 for like notifications, 2 for comment notifications, and 3 for follow notifications. We will hold a to_user and a from_user. The to_user will receive the notification and the from_user is the sending user. We will also have a post and comment field, this is where we will hold the post or comment that the user added a like or comment to. If they follow a user, these two fields will be blank. We also need a date, this will hold the date of the notification. Finally, we will hold a boolean field to check if the user has already seen the notification.

class Notification(models.Model):
# 1 = Like, 2 = Comment, 3 = Follow
notification_type = models.IntegerField(null=True, blank=True)
to_user = models.ForeignKey(User, related_name='notification_to', on_delete=models.CASCADE, null=True)
from_user = models.ForeignKey(User, related_name='notification_from', on_delete=models.CASCADE, null=True)
post = models.ForeignKey('Post', on_delete=models.CASCADE, related_name='+', blank=True, null=True)
comment = models.ForeignKey('Comment', on_delete=models.CASCADE, related_name='+', blank=True, null=True)
date = models.DateTimeField(default=timezone.now)
user_has_seen = models.BooleanField(default=False)

Add Notification to Admin.py & Create Test Notifications

Now that we have our model set up, let’s add it to the admin panel and create a couple test notifications and try to display them.

admin.site.register(Notification)

Add the List of Notifications Using Custom Tag

Make sure to go into the admin panel to add a couple notifications to test that this next step is working. Now let’s display our notifications in our navbar. We will create a custom template tag to use in our template. To do this we need to create a folder in our landing app called templatetags and there needs to be two files in there an __init__.py file and a file that will hold our custom tags. We will call ours custom_tags.py. Inside of the custom tags file, we will create an inclusion tag. This will be a that includes HTML in whatever template that we add it to, but what this will allow us to do is create custom logic to get what we need without creating another view. We can get the user from the context and then we can filter the notifications by the user and if it has not been seen. We can then return these notifications in the context. Now we can access them like we can from any view that we created before.

from django import template
from social.models import Notification
register = template.Library()@register.inclusion_tag('social/show_notifications.html', takes_context=True)
def show_notifications(context):
request_user = context['request'].user
notifications = Notification.objects.filter(to_user=request_user).exclude(user_has_seen=True).order_by('-date')
return {'notifications': notifications}

Let’s include this right below the profile dropdown.

<div class="nav-item">
{% show_notifications %}
</div>

Now let’s list out all of these notifications. I am just going to put them in a dropdown menu. This might look like a lot but it really is the same as any of our other templates where we have a for loop and if statements, there are just a lot of them to account for every notification type.

{% load static %}<div class="dropdown-content d-none" id="notification-container">
{% for notification in notifications %}
{% if notification.post %}
{% if notification.notification_type == 1 %}
<div class="dropdown-item-parent">
<a href="{% url 'post-notification' notification.pk notification.post.pk %}">@{{ notification.from_user }} liked your post</a>
<span class="dropdown-item-close" onclick="removeNotification(`{% url 'notification-delete' notification.pk %}`, `{{ request.path }}`)">&times;</span>
</div>
{% elif notification.notification_type == 2 %}
<div class="dropdown-item-parent">
<a href="{% url 'post-notification' notification.pk notification.post.pk %}">@{{ notification.from_user }} commented on your post</a>
<span class="dropdown-item-close" onclick="removeNotification(`{% url 'notification-delete' notification.pk %}`, `{{ request.path }}`)">&times;</span>
</div>
{% endif %}
{% elif notification.comment %}
{% if notification.notification_type == 1 %}
<div class="dropdown-item-parent">
<a href="{% url 'post-notification' notification.pk notification.comment.post.pk %}">@{{ notification.from_user }} liked your comment</a>
<span class="dropdown-item-close" onclick="removeNotification(`{% url 'notification-delete' notification.pk %}`, `{{ request.path }}`)">&times;</span>
</div>
{% endif %}
{% if notification.notification_type == 2 %}
<div class="dropdown-item-parent">
<a href="{% url 'post-notification' notification.pk notification.comment.post.pk %}">@{{ notification.from_user }} replied to your comment</a>
<span class="dropdown-item-close" onclick="removeNotification(`{% url 'notification-delete' notification.pk %}`, `{{ request.path }}`)">&times;</span>
</div>
{% endif %}
{% else %}
<div class="dropdown-item-parent">
<a href="{% url 'follow-notification' notification.pk notification.from_user.profile.pk %}">@{{ notification.from_user }} has started following you</a>
<span class="dropdown-item-close" onclick="removeNotification(`{% url 'notification-delete' notification.pk %}`, `{{ request.path }}`)">&times;</span>
</div>
{% endif %}
{% endfor %}
</div>
</div>

There are links in here that we haven’t made yet, we will take care of that in a second. We also used some custom styles there, let’s add some CSS styles to make the dropdown menu look better.

.dropdown {
position: relative;
display: inline-block;
}

.dropdown-content {
position: absolute;
background-color: #f1f1f1;
min-width: 300px;
box-shadow: 0px 8px 8px 0px rgba(0,0,0,0.2);
z-index: 1;
font-size: 0.9rem;
}

.dropdown-content a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
}

.dropdown-content a:hover {background-color: #ddd;}
.dropdown-item-parent {
position: relative;
display: block;
}

.dropdown-item-close {
position: absolute;
top: 0;
right: 0;
font-size: 1.8rem;
padding-right: 5px;
transition: 0.3s;
}
.dropdown-item-close:hover {
color: rgb(180, 25, 25);
transition: 0.3s;
cursor: pointer;
}

Show Notification Badge & Add Hover Effects

Now that we have our notification drop down menu working, we need some way to open and close this menu. I am going to add a Bootstrap 5 badge that will hold the number of unseen notifications. When the user clicks on the badge it will show or hide the dropdown menu. Let’s start with adding the badge and the styles for it.

<!-- BOOTSTRAP 5 -->
<div class="dropdown">
<span class="badge bg-primary notification-badge" onclick="showNotifications()">{{ notifications.count }}</span>

Now let’s add the styles for this notification badge

.notification-badge {
transition: 0.3s;
}
.notification-badge:hover {
cursor: pointer;
opacity: 0.75;
transition: 0.3s;
}

Add Javascript to Hide or Show Notifications

Notice in the HTML for the badge that we added an event listener for a function called showNotifications(). This will work similar to the comment reply toggle that we created a while ago. Let’s add this now to our social.js

function showNotifications() {
const container = document.getElementById('notification-container');
if (container.classList.contains('d-none')) {
container.classList.remove('d-none');
} else {
container.classList.add('d-none')
}
}

This will just find the container with the id of notification-container and either show or hide it to toggle it.

Notification Views & Redirect to Post or Profile

Now that we have a dropdown and a way to toggle the dropdown, we now need to add a view that will mark the notification as seen and redirect to whatever the notification is pointing to. Then like we usually do, we will add a URL path for it. Finally we will add a link in the dropdown to actually go to that view, which will then redirect to the post or profile on the notification.

class PostNotification(View):
def get(self, request, notification_pk, object_pk, *args, **kwargs):
notification = Notification.objects.get(pk=notification_pk)
post = Post.objects.get(pk=object_pk)
notification.user_has_seen = True
notification.save()
return redirect('post-detail', pk=object_pk)class FollowNotification(View):
def get(self, request, notification_pk, object_pk, *args, **kwargs):
notification = Notification.objects.get(pk=notification_pk)
profile = UserProfile.objects.get(pk=object_pk)
notification.user_has_seen = True
notification.save()
return redirect('profile', pk=object_pk)

We created two different views, the PostNotification view will handle both Posts and Comments since they do the same thing. Then we have a FollowNotification view. This will redirect to the profile of the user that followed the logged in user. Let’s add a URL path for these and make the name match what we have in our show_notifications tempate.

path('notification/<int:notification_pk>/post/<int:object_pk>', PostNotification.as_view(), name='post-notification'),path('notification/<int:notification_pk>/follow/<int:object_pk>', FollowNotification.as_view(), name='follow-notification'),

Create Notification after Like, Comment or Follow

Now let’s add the logic to send a notification when one of the three types happen. We need to make sure on our like views to put it inside of the if statement that actually adds the like.

In our AddCommentLike view:

if not is_like: 
comment.likes.add(request.user)
notification = Notification.objects.create(notification_type=1, from_user=request.user, to_user=comment.author, comment=comment)

In our AddLike view:

if not is_like: 
post.likes.add(request.user)
notification = Notification.objects.create(notification_type=1, from_user=request.user, to_user=post.author, post=post)

And finally in our PostDetailView post method, the CommentReplyView, and the AddFollower view add a line at the bottom of the file. Here is what the AddFollower line should look like. The others will just need to pass in whatever post or comment is being added.

notification = Notification.objects.create(notification_type=3, from_user=request.user, to_user=profile.user)

Remove Notifications

Finally, we want to use the X button that we added to the notifications dropdown to mark a notification as seen and remove if from the list. Let’s create the view for this first and then we will create the URL pattern for it. After that, we need to add some javascript to send the request.

class RemoveNotification(View):
def delete(self, request, notification_pk, *args, **kwargs):
notification = Notification.objects.get(pk=notification_pk)
notification.user_has_seen = True
notification.save()
return HttpResponse('Success', content_type='text/plain')
path('notification/delete/<int:notification_pk>', RemoveNotification.as_view(), name='notification-delete')
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function removeNotification(removeNotificationURL, redirectURL) {
var xmlhttp = new XMLHttpRequest();
const csrftoken = getCookie('csrftoken');
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == XMLHttpRequest.DONE) {
if (xmlhttp.status == 200) {
window.location.replace(redirectURL);
}
else {
alert('There was an error.');
}
}
};
xmlhttp.open("DELETE", removeNotificationURL, true);
xmlhttp.setRequestHeader("X-CSRFToken", csrftoken)
xmlhttp.send();
}

In this Javascript file, we need to do two things, first get the CSRF token. Next we need to send an AJAX request to the view we just created. We will create a new request, and set it to redirect to whatever we passed in as the redirect URL if it receives a 200 response. Below that reqeust we need to actually send it and send a DELETE request since that is what we set the HTTP method as in our view. We also need to add a header that contains the csrf token that we got from the previous function.

This should conclude adding notifications to our application now, we should be able to alert the user of new activity at the top of every page.