How to set up user notifications for your Django App - Part 2
Notifications provide updates about the activity on an application. In this guide, you will learn how to set up simple notifications for your Django application. This is Part 2 of 2 in this tutorial series. We will be focusing on creating the notifications components for our frontend app.
Themba Mahlangu

Introduction
Notifications provide updates about the activity on an application. In this guide, you will learn how to set up simple notifications for your Django application. This is Part 2 of this series.
In this tutorial, we will be focusing on creating components for our frontend app.
Prerequisites
To complete this tutorial, you will need:
- Django starter project with users and notifications already set up from Part 1 of this series.
- Basic knowledge of Django, Django Templates, and Alpine JS.
Recap
In the last tutorial, we set up a backend to store and manage the business logic for user notification/activity. We will pick up where we left off and create frontend components for displaying user information.
Folder Structure & Base Templates
Here is the project folder structure:
my_proj
├── config
├── apps
├── templates
│ ├── base.html
│ ├── dashboard.html
│ └── components
│ ├── navbar.html
│ ├── user_notifications.html
└── manage.pySet Up Our Generic Template - Base
Here is the base.html file:
<!--base.html-->
<html>
<head>
<!--TailwindCSS-->
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
<!--Alpinejs--->
<script defer src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v3.0.1/dist/alpine.min.js"></script>
<!--Other common js for example --->
<script>
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;
}
</script>
</head>
<body>
{% include "components/navbar.html" %}
{% block content %}{% endblock %}
{% block footer_js %}{% endblock %}
</body>
</html>The base.html file is the main template for the Django project.
It includes the necessary CSS and JS files.
A navbar is included in the base.html file.
Blocks for content and footer JS are included, which can be overridden in other templates.
Create Your Components
Here is the navbar.html file:
<!--navbar.html-->
{% load static i18n %}
<div class="w-screen flex flex-row items-center p-1 justify-between bg-white shadow-xs">
<div class="ml-8 text-lg text-gray-700 hidden md:flex">My Website</div>
<div class="flex flex-row-reverse mr-8 hidden md:flex">
{% if not request.user.is_authenticated %}
<a class="text-gray-700 text-center bg-gray-400 px-4 py-2 m-2" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
<a class="text-gray-700 text-center bg-gray-400 px-4 py-2 m-2" href="{% url 'account_login' %}">{% trans "Sign In" %}</a>
{% else %}
{% include "components/dropdowns/user_notifications.html" %}
{% endif %}
</div>
</div>And here is the user_notifications.html file:
<div class="relative flex align-center justify-center mr-4" x-data="user_notification_dropdown" >
<!-- navbar button -->
<button @click="toggle()" class="p-0 rounded-full focus:outline-none focus:ring-indigo">
<span class="sr-only">View notifications</span>
<svg class="h-6 w-6 m-1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg>
</button>
<!-- purple dot showing user has unread notifications -->
<div x-show="hasUnreadNotifications" class="absolute right-0 p-1 bg-indigo-300 rounded-full bottom-3 animate-ping" x-cloak></div>
<div x-show="hasUnreadNotifications" class="absolute right-0 p-1 bg-indigo-500 border border-white r rounded-full bottom-3" x-cloak></div>
<div x-show="open" x-cloak @click.away="open = false" class="absolute z-10 mt-12 w-screen max-w-md sm:px-0 transform -translate-x-48" >
<div class="rounded-lg shadow-lg ring-1 ring-black ring-opacity-5 max-h-dash-sm overflow-y-scroll relative">
<div class="px-5 py-5 bg-gray-50 space-y-6 sm:flex sm:space-y-0 sm:space-x-10 sm:px-8 sticky top-0 z-30">
<div class="flow-root">
<a href="#" class="-m-3 p-3 flex items-center rounded-md text-base font-medium text-gray-900 hover:bg-gray-100">
<span class="ml-3">Activity</span>
</a>
</div>
</div>
<!-- list notifications -->
<div class="relative grid gap-6 bg-white px-5 py-6 sm:gap-8 sm:p-8">
<template x-for="(item, index) in notifications">
<a @click="" class="-m-3 p-3 flex justify-between rounded-lg hover:bg-gray-50">
<div class="ml-4">
<p class="text-base font-medium text-gray-900" x-text="item.verb"></p>
<p class="mt-1 text-sm text-gray-500" x-text="`From ${item.actor}`"></p>
</div>
<div>
<template x-if="item.unread">
<div class="badge badge-sm badge-success">unread</div>
</template>
<template x-if="!item.unread">
<div class="badge badge-sm badge-ghost">read</div>
</template>
</div>
</a>
</template>
<template x-if='notifications.length === 0'>
<a class="-m-3 p-3 flex justify-between rounded-lg hover:bg-gray-50">
<div class="ml-4">
<p class="text-base font-medium text-gray-900"> You have notifications</p>
<p class="mt-1 text-sm text-gray-500">No Notifications</p>
</div>
</a>
</template>
</div>
<div class="px-2 py-2 space-y-6 flex bg-white justify-center sm:space-y-0 sm:space-x-10 sm:px-8 z-30 border-t">
<div class="flow-root">
<a href="{% url "view-name-to-reverse-to-some-view--here" %}" class="-m-3 p-3 flex items-center rounded-md blue-500 font-medium hover:bg-gray-100">
<span class="ml-3">Go to notifications page</span>
</a>
</div>
</div>
</div>
</div>
</div>The navbar.html and user_notifications.html files are included in this section.
The navbar.html file is included in the base.html file and includes links for user authentication and a dropdown for user notifications.
The user_notifications.html file includes the HTML for the user notifications dropdown.
Connect Your Component to the Backend
Now that we have declared our HTML, let’s add the logic to fetch user notifications from the backend.
<script>
const csrf_token = getCookie('csrftoken');
document.addEventListener('alpine:initializing', () => {
Alpine.data('user_notifications_dropdown', () => ({
/** Component data **/
open: false, //drop down state
hasUnreadNotifications: false, // purple dot will show if this is true
notifications: [], // list of notifications
/** Component methods **/
init(){
fetch('inbox/notifications/api/all_list/', {
method: 'GET',
credentials: 'same-origin',
headers:{
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrf_token,
},
}).then(response => response.json())
.then(data => {
this.notifications = data.all_list
this.hasUnreadNotifications = data.all_list.filter(item => item.unread === true).length > 0
}).catch(err => console.log(err));
},
toggle() {
this.open = ! this.open
}
}))
})
</script>The JavaScript code is included within a script tag in the HTML file. This code is responsible for fetching user notifications from the backend and updating the component's state.
The csrf_token is a security feature in Django that prevents cross site request forgery. It's fetched from the cookies and included in the headers of the fetch request for authentication.
The document.addEventListener('alpine:initializing', () => {...}) is an event listener that runs the enclosed function when Alpine.js is initializing. Alpine.js is a minimal JavaScript framework used for creating interactive frontend components.
Inside the event listener, Alpine.data('user_notifications_dropdown', () => ({...})) is used to create a new Alpine.js component named 'user_notifications_dropdown'.
The component has several data properties: open, hasUnreadNotifications, and notifications.
open is a boolean that controls the visibility of the dropdown,
hasUnreadNotifications is a boolean that indicates whether there are any unread notifications, and
notifications is an array that stores the fetched notifications.
The component also has two methods: init and toggle.
The init method is called when the component is initialized. It sends a GET request to the backend to fetch the notifications and updates the component's state based on the fetched data. The toggle method is used to toggle the visibility of the dropdown.
The fetch request is sent to the 'inbox/notifications/api/all_list/' URL. The request headers include the CSRF token for authentication and specify that the expected response is in JSON format. If the request is successful, the response is converted to JSON and used to update the component's state. If the request fails, the error is logged to the console.
Bringing It Together
Finally, let’s set up our page so we see the fruits of our labour.
Add another route to your urls.py file:
from django.urls import path
from django.views.generic import TemplateView
import notifications.urls
urlpatterns = [
...
path('inbox/notifications/', include(notifications.urls, namespace='notifications')),
path("/dashboard", TemplateView.as_view(template_name="layouts/dashboard.html"), name="dashboard", )
]And here is the dashboard.html file:
{% extends 'base.html' %}
{% block title %}Dashboard {% endblock %}
{% block content %}
<h1>Dashboard</h1>
{% endblock %}
{% block footer_js %}
<script>
const csrf_token = getCookie('csrftoken');
document.addEventListener('alpine:initializing', () => {
Alpine.data('user_notifications_dropdown', () => ({
..... SEE STEP #3 ABOVE FOR THIS SECTION ......
}))
})
</script>
{% endblock %}Conclusion
And that’s it. We now have a working user_notifications dropdown component. As our app grows we can easily extend this to improve functionality, for example, add real-time capability using WebSockets or improve UX e.g. mark notifications as ‘read’ on hover. If you want to learn how to quickly deploy a production-ready WebSockets server, check out our article on this.