In this tutorial, we will build Django and JavaScript CRUD App. For demonstration purposes, we will be creating a Notes Application that lets you log in and store notes to access them from anywhere!

After this tutorial, our site should work something like this:

Prerequisites

  • Install Python
  • Install pip
  • Have an IDE that supports Python and JavaScript
  • Basic knowledge of Python and JavaScript
  • Basic Knowledge of GET and POST requests.
  • Basic knowledge of RDBMS (Relational Database Management System) (Optional)
  • Knowledge of virtual environments or venv (Optional)

Note

  • In the following tutorial, <project_name> refers to your project name and <app_name> refers to your app name.
  • This tutorial does not explain how the HTML files are made. It is assumed that the reader knows basic HTML and Bootstrap (Optional).

Install Django

Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. Built by experienced developers, it takes care of much of the hassle of web development, so you can focus on writing your app without needing to reinvent the wheel. It’s free and open source.

To install Django, enter the following command after preferably activating a virtual environment,

python -m pip install Django

Make sure you can import Django after installing it. If it’s not installed, try installing again.

Setting Up

To start a Django project, go to your preferred directory and type the following command:

django-admin startproject <project_name>

Remember:- Replace <project_name> with what you want to name your project.

Now, cd into your project directory with the following command:

cd <project_name>

Now we will create our app with the following command:

django-admin startapp <app_name>

Note:- Make sure to give different names to your project and your app.

Inside your project directory, there will be another app with the name of your project, inside which there is a settings.py file. Go inside the setttings.py file and look for the INSTALLED_APPS variable and add your app name to the end of that list. 

Now inside the <app_name> directory for the app which you created using startapp, create two folders named templates and static respectively, and a file named urls.py. Inside the templates and static folders, create another directory with the <app_name>. Templates will store your HTML files and the static directory will store your script and CSS(Optional) files. The urls.py file will store your paths.

Add the below code to your urls.py file

from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('register/', views.register, name='register'),
    path('login/', views.login_view, name='login'),
    path('logout/', views.logout_view, name='logout'),
]

Our project currently contains 4 paths that facilitate register, login, logout, and index. We will add more paths later in this tutorial.

Go into the <project_name> app and find the URLs file. Inside that, add the following lines to add the URLs from your <app_name> app into your project.

from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path("", include("<app_name>.urls")),
]

Note:- Some lines in the above code should already be there. Just make changes for the code to match.

Add this to your settings.py file to prevent clashes with the reverse accessor for auth.User.groups

`AUTH_USER_MODEL = "<app_name>.User"`

Templates

All HTML files should be stored in the <app_name>/templates/<app_name> path. There are 4 HTML files in my project:-

  • layout.html (Basic layout visible in all pages)
  • login.html
  • register.html
  • index.html

These files look as follows:

layout.html


    {% load static %}
    
    <!DOCTYPE html>
    
    <html lang="en">
      <head>
          <meta charset="UTF-8">
          <meta http-equiv="X-UA-Compatible" content="IE=edge">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <meta name="description" content="Single Page Application to store notes and access them from anywhere!">
          <title>{% block title %} Notes {% endblock %}</title>
          <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
          <link rel="stylesheet" href="{% static 'notes_app/styles.css' %}">
          {% block script %}{% endblock %}
      </head>
      <body>
          <nav class="sticky-top navbar navbar-light bg-light">
              <div class="nav-left">
                  <a href="/" class="nav-item navbar-text logo-text navbar-brand">Notes</a>
              </div>
              <div class="nav-right">
                  {% if user.is_authenticated %}
                      <a href="{% url 'logout' %}" class="nav-item nav-link">Logout</a>
                  {% else %}              
                      <a href="{% url 'register' %}"class="nav-item nav-link">Register</a>
                      <a href="{% url 'login' %}" class="nav-item nav-link">Login</a>
                  {% endif %}
              </div>
          </nav>
          {% block body %} {% endblock %}
      </body>
    
    </html>

login.html


    {% extends "notes_app/layout.html" %}
    
    {% block body %}
        <div class="login">
            <h2>Login</h2>
            {% if message %}
                <div>{{ message }}</div>
            {% endif %}
            <form class="form-login" action="{% url 'login' %}" method="post">
                {% csrf_token %}
                <div class="form-group">
                    <input autofocus class="form-control" type="email" name="email" placeholder="Email">
                </div>
                <div class="form-group">
                    <input autocomplete="current-password" class="form-control" type="password" name="password" placeholder="Password">
                </div>
                <input class="btn" type="submit" value="Login">
            </form>
            <div>Don't have an account? <a href="{% url 'register' %}">Register here.</a></div>
        </div>
    {% endblock %}

register.html


{% extends 'notes_app/layout.html' %}

{% block body %}
    <div class="register">
        <h2>Register</h2>
        {% if message %}
            <div>{{ message }}</div>
        {% endif %}
        <form class="form-register" action="{% url 'register' %}" method="post">
            {% csrf_token %}
            <div class="form-group">
                <input class="form-control" type="email" name="email" placeholder="Email Address">
            </div>
            <div class="form-group">
                <input autocomplete="new-password" class="form-control" type="password" name="password" placeholder="Password">
            </div>
            <div class="form-group">
                <input autocomplete="new-password" class="form-control" type="password" name="confirmation" placeholder="Confirm Password">
            </div>
            <input class="btn" type="submit" value="Register">
        </form>
        <div>Already have an account? <a href="{% url 'login' %}">Log In here.</a></div>
    </div>

{% endblock %}

index.html

{% extends "notes_app/layout.html" %}

{% load static %}

{% block script %}<script src="{% static 'notes_app/notes.js' %}" defer></script>{% endblock %}

{% block body %}
    <div class="add">
        <form method="post">
            <div>
                <input type="text" name="title" placeholder="Title">
            </div>
            <!-- <input type="textarea" name="content" placeholder="Content"> -->
            <div class="addcontent">
                <textarea name="content" placeholder="Content"></textarea>
            </div>
            <div>
                <input type="submit" value="Add" data-submit="true">
            </div>
        </form>
    </div>
    <div class="notes"></div>
{% endblock %}

Models

Models are the database for Django applications. The fields and tables are defined in the models.py file in the <app_name> folder. Our application will utilize two models. The code for our models is given below.

The Notes model has a foreign key which refers to the User model. We will use this relationship between these two models to display only the user’s own notes to them.

Now, it is important to make migrations every time you make changes to the models.py file. To do this, type the following commands.

Login, Register, Logout, and Index Views

First, we will import all the required modules and functions we will need for our views.

from django.shortcuts import render
from django.contrib.auth import authenticate, login, logout
from django.db import IntegrityError
from django.urls import reverse
from django.http import HttpResponseRedirect, JsonResponse
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
from json import loads
from notes_app.models import User, Notes

Now we will write the register view,

def register(request):
    if request.method == "POST":
        email = request.POST["email"]
        # Ensure password matches confirmation
        password = request.POST["password"]
        confirmation = request.POST["confirmation"]
        if password != confirmation:
            return render(request, "notes_app/register.html", {
                "message": "Passwords must match."
            })
        # Attempt to create new user
        try:
            user = User.objects.create_user(username = email, password = password)
            user.save()
        except IntegrityError:
            return render(request, "notes_app/register.html", {
                "message": "User with the entered email already exists."
            })
        login(request, user)
        return HttpResponseRedirect(reverse("index"))
    return render(request, 'notes_app/register.html')

When a form is submitted on the /register/ path, it first collects the email, password, and confirmation password. It then checks if the password and confirmation password is the same. It then attempts to create a new user. Django raises an Integrity Error if a user with the given username(email) already exists. On successful creation of a new user, it logs in the user by calling the login function in django.contrib.auth and redirects the user to the index page.

Now let’s write the login view,


def login_view(request):
    if request.method == "POST":
        # Attempt to sign user in
        email = request.POST["email"]
        password = request.POST["password"]
        user = authenticate(request, username=email, password=password)
        # Check if authentication successful
        if user is not None:
            login(request, user)
            return HttpResponseRedirect(reverse("index"))
        else:
            return render(request, "notes_app/login.html", {
                "message": "Invalid email and/or password."
            })
    return render(request, 'notes_app/login.html')

The above view tries to authenticate the user by calling the authenticate function in django.contrib.auth,  which returns None on failure.

Now let’s write the logout view

def logout_view(request):
    logout(request)
    return HttpResponseRedirect(reverse("login"))

This logs out the currently logged-in user and redirects the user to the login page.

Now we will write the index view.

@login_required(login_url='/login/')
def index(request):
    return render(request, 'notes_app/index.html')

This view renders the index page if the user is logged in, else redirects to /login/.

Django and JavaScript CRUD

A logged-in user can add a note by filling in the fields in the form on the index page. To add and delete a note, let’s add three new URLs that our JavaScript code can send data.

path('notes/', views.notes, name='notes'),
path('delete/', views.delete, name='delete'),
path('edit/', views.edit, name='edit'),

And a view to actually add and read notes to and from the database.

@login_required(login_url='/login/')
@csrf_exempt
def notes(request):
    if request.method == "GET":
        try:
            user = request.user
            notes = user.notes.all()
            notes_list = []
            for i in notes:
                a = {'id': i.id, 'title': i.title,'note': i.note,'created_at': i.created_at,'updated_at': i.updated_at}
                notes_list.append(a)       
            return JsonResponse({"status": 0, "notes": notes_list})
        except:
            return JsonResponse({"status": 1})
    try:
        note = loads(request.body)
        title = note["title"]
        content = note["content"]
        user = request.user
        note_object=  Notes.objects.create(user=user, title=title, note=content)
        note_object.save()
        return JsonResponse({"status": 0})
    except:
        return JsonResponse({"status": 1})

This view adds a note to the database when called using a POST request, otherwise returns all the notes of the currently logged-in user.

Edit View

@login_required(login_url='/login/')
@csrf_exempt
def edit(request):
    if request.method == "POST":
        try:
            note_edit = loads(request.body)
            note = Notes.objects.get(id=note_edit["id"])
            note.title = note_edit["title"]
            note.note = note_edit["content"]
            note.save()
            return JsonResponse({"status": 0})
        except:
            return JsonResponse({"status": 1})

The edit view gets the note from the database by its id and update its title and note with the data sent in the POST request. It then saves the updated note in the database and returns status 0 on a successful update.

Delete View

@login_required(login_url='/login/')
@csrf_exempt
def delete(request):
    if request.method == "POST":
        try:
            note_id = loads(request.body)
            note_id = note_id["id"]
            note = Notes.objects.filter(id=note_id)
            if len(note) != 0:
                note.delete()
            return JsonResponse({"status": 0})
        except:
            return JsonResponse({"status": 1})

The delete view receives the id of the note to delete. It then finds the note with that id and returns status 0 upon successful deletion.

Now, let’s add JavaScript to our web application to asynchronously update our website by fetching data from our Django Server.

updateNoteList

async function updateNoteList() {
    const response = await fetch('/notes/');
    const data = await response.json();
    if (data.status == 0) {
        const notes = data.notes;
        const notesList = document.querySelector(".notes");
        notesList.innerHTML = "";
        notes.forEach(note => {
            const noteElement = document.createElement("div");
            noteElement.classList.add("note")
            noteElement.dataset.id = note.id;
            const noteTitle = document.createElement("div");
            noteTitle.classList.add("note-title");
            noteTitle.dataset.id = note.id;
            noteTitle.textContent = note.title;
            noteElement.appendChild(noteTitle);
            const noteContent = document.createElement("div");
            noteContent.classList.add("note-content");
            noteContent.dataset.id = note.id;
            noteContent.textContent = note.note;
            noteElement.appendChild(noteContent);
            const noteActions = document.createElement("div");
            noteActions.classList.add("note-actions");
            noteActions.dataset.id = note.id;
            const noteEdit = document.createElement("button");
            noteEdit.classList.add("note-edit");
            noteEdit.dataset.id = note.id;
            noteEdit.textContent = "Edit";
            noteActions.appendChild(noteEdit);
            const noteDelete = document.createElement("button");
            noteDelete.classList.add("note-delete");
            noteDelete.dataset.id = note.id;
            noteDelete.textContent = "Delete";
            noteActions.appendChild(noteDelete);
            noteElement.appendChild(noteActions);
            notesList.appendChild(noteElement);
        });
    }
}

The updateNoteList function sends a GET request to /notes/ to get all notes by the currently logged-in user and displays it by creating HTML elements in our website and setting their properties appropriately.

addOnClicks


async function addOnClicks() {
    document.querySelectorAll(".note-delete").forEach(button => {
        button.addEventListener('click', async e => {
            const noteId = {id: e.target.dataset.id};
            let request = await fetch(`/delete/`, {
                method: 'POST',
                body: JSON.stringify(noteId)
            })
            request = await request.json();
            if (request.status == 0) {
                await updateNoteList();
                await addOnClicks();
            }
        });
    })
    document.querySelectorAll(".note-edit").forEach(button => {
        button.addEventListener('click', async e => {
            const noteId = {id: e.target.dataset.id};
            const noteElement = document.querySelector(`.note[data-id="${noteId.id}"]`);
            const noteTitleElement = document.querySelector(`.note-title[data-id="${noteId.id}"]`);
            const noteContentElement = document.querySelector(`.note-content[data-id="${noteId.id}"]`);
            const noteContentEdit = document.createElement("textarea");
            noteContentEdit.classList.add("note-content-edit");
            noteContentEdit.dataset.id = noteId.id;
            noteContentEdit.value = document.querySelector(`.note-content[data-id="${noteId.id}"]`).textContent;
            noteContentEdit.placeholder = "Content";
            noteContentEdit.style.display = "block";
            noteContentElement.style.display = "none";
            noteElement.insertBefore(noteContentEdit, noteContentElement);
            const noteTitleEdit = document.createElement("input");
            noteTitleEdit.classList.add("note-title-edit");
            noteTitleEdit.dataset.id = noteId.id;
            noteTitleEdit.type = "text";
            noteTitleEdit.name = "title";
            noteTitleEdit.placeholder = "Title";
            noteTitleEdit.value = document.querySelector(`.note-title[data-id="${noteId.id}"]`).textContent;
            noteTitleElement.style.display = "none";
            noteElement.insertBefore(noteTitleEdit, noteTitleElement);
            const noteCancelEditButton = document.createElement("button");
            noteCancelEditButton.classList.add("note-cancel-edit");
            noteCancelEditButton.dataset.id = noteId.id;
            noteCancelEditButton.innerHTML = "&#x2715;";
            noteCancelEditButton.addEventListener('click', async () => {
                noteElement.removeChild(noteTitleEdit);
                noteElement.removeChild(noteContentEdit);
                noteTitleElement.style.display = "block";
                noteContentElement.style.display = "block";  
                const noteActions = document.querySelector(`.note-actions[data-id="${noteId.id}"]`);
                noteActions.removeChild(noteSaveButton);
                noteActions.removeChild(noteCancelEditButton);
            })
            const noteSaveButton = document.createElement("button");
            noteSaveButton.classList.add("note-save");
            noteSaveButton.dataset.id = noteId.id;
            noteSaveButton.textContent = "Save";
            noteSaveButton.addEventListener('click', async e => {
                const note = {
                    id: e.target.dataset.id,
                    title: document.querySelector(`.note-title-edit[data-id="${noteId.id}"]`).value,
                    content: document.querySelector(`.note-content-edit[data-id="${noteId.id}"]`).value
                };
                if (note.content.length === 0) return
                let request = await fetch(`/edit/`, {
                    method: 'POST',
                    body: JSON.stringify(note)
                })
                request = await request.json();
                if (request.status == 0) {
                    noteElement.removeChild(noteTitleEdit);
                    noteElement.removeChild(noteContentEdit);
                    noteTitleElement.textContent = note.title;
                    noteTitleElement.style.display = "block";
                    noteContentElement.textContent = note.content;
                    noteContentElement.style.display = "block";  
                    const noteActions = document.querySelector(`.note-actions[data-id="${note.id}"]`);
                    noteActions.removeChild(noteSaveButton);
                    noteActions.removeChild(noteCancelEditButton);
                }
            });
            document.querySelector(`.note-actions[data-id="${noteId.id}"]`).appendChild(noteSaveButton);
            document.querySelector(`.note-actions[data-id="${noteId.id}"]`).appendChild(noteCancelEditButton);
        });
    })
}

The addOnClicks function adds an event listener to all delete and edit buttons. When a user clicks on the edit button, our JavaScript code replaces note title and content divs with input and textarea elements, respectively.

It prefills those elements with the current value of the note and the user can edit it. Clicking on the edit button also creates a save button with a ‘click’ eventListener.

When the user clicks on save after editing their note, the button sends a POST request to /edit/ path with the updated note. When a user clicks on the delete button, the button sends a POST request to /delete/ with the note id.

Add Note Form EventListener

document.querySelector("form").addEventListener('submit', async e => {
    e.preventDefault();
    const title = document.querySelector("form input[name='title']").value;
    const content = document.querySelector("form input[name='content']").value;
    if (content.length == 0) return
    const note = {
        title: title,
        content: content
    };
    let request = await fetch('/notes/', {
        method: 'POST',
        body: JSON.stringify(note)
    })
    request = await request.json();
    if (request.status == 0) {
        document.querySelectorAll("form input").forEach(field => (!field.dataset["submit"]) ? field.value = "": null);
        await updateNoteList();
        await addOnClicks();
        return;
    }
});

When the form is submitted, our code first checks if the note content is empty(the title can be empty). If the note content is not empty, it sends a post request to /notes/ with the form data. 

Now, finally calling the updateNoteList and addOnClicks functions.

document.addEventListener('DOMContentLoaded', async () => {
    await updateNoteList();
    addOnClicks();
})

The above lines of code calls the updateNoteList and the addOnClicks functions when the DOMContentLoaded event is triggered.

After adding some CSS, this is how our site looks.

 Django and JavaScript CRUD
Django and JavaScript Crud app

Check out the code on GitHub

Summary

  • We created a Single-Page CRUD (Create, Read, Update, Delete) application using Django and JavaScript.
  • We created a Notes app that lets you log in, add a note, and view, edit and delete it.
  • We used Django models to store user data.
  • We used the fetch API to make requests to our server for data.