A. Maharjan

Learn Django over the weekend

Learn Django over the Weekend

In this article, I will help you learn Django and guide you through creating a simple Django web project over the weekend. We will focus on building an easy and user-friendly password manager web application. We'll call this project easy-password-manager.

Level

  • Beginner

Prerequisite

  • Basic Linux command
  • Basic Python3 knowledge

Table of contents

Mockup

The project design and layout includes:

  1. Login page ↗
  2. Index page ↗
  3. Detail page ↗
  4. Update page ↗
  5. Delete page ↗

What is Django?

Django is a powerful Python web framework designed for rapid web developement and clean, pragmatic design [1]. According to [2], Django stands out as the second best backend framework for web development. Flask is another popular framework that serves as an alternative to Django.

What is MVT in Django?

Django is built upon the MVT (Model-View-Template) architecture, which is a software design pattern used for web application development [3]. Figure 1 illustrates the MVT architecture for Django.

Django MVT (Model-View-Template) architecture

figure 1. Model-View-Template architecture for Django, own representation based on [4] and [5].

Next, I will quickly delve into these three "seperation of concerns"

1. Model

The model acts as the data interface, responsible for data migration and maintenance [6].

2. View

The view is where all application logic is written.

3. Template

The template is the front-end layer comprising visual components for the application [7].

Project Initialization

Before starting the project, make sure that Python3 is already installed on your machine

$ python3 --version

Create directories

$ mkdir easy-password-manager
$ cd easy-password-manager

git

Make sure git is installed and easy-password-manager is initialized

$ git init .

Create .gitignore file

$ touch .gitignore
$ vi .gitignore

Save the following list into .gitignore

venv
__pycache__

The file specifies intentionally untracked folders/files that Git should ignore [8]. Therefore, any files mentioned inside the .gitignore will be excluded.

venv

The python3 venv module helps create and manage "virtual environments" [9]. Now, let's setup venv:

$ python3 -m venv venv

Activate venv

$ source venv/bin/activate

Django setup

Now, time to install Django

(venv) $ pip3 install django
(venv) $ pip3 freeze > requirements.txt

Create a new Django project called easy_password_manager

(venv) $ django-admin startproject easy_password_manager .

startapp

Furthermore, create an app called passwordmanager. "Django uses the concept of projects and apps to keep code clean and readable" [10].

(venv) $ python3 manage.py startapp passwordmanager

We have some untracked files, let's git commit

(venv) $ git status

Untracked files:
    .gitignore
    easy_password_manager/
    manage.py
    passwordmanager/
    requirements.txt
(venv) $ git add .
(venv) $ git commit -m "initial updates"

Let's look into the current folder structure, and I'll explain what those each file is used for

.
├── easy_password_manager
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
├── passwordmanager
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   ├── models.py
│   ├── tests.py
│   └── views.py
└── venv

(1) asgi.py: Asynchronous Server Gateway Interface [11].

(2) settings.py: To store configuration for Django project [12].

(3) urls.py: Consist of urlpatterns list, which is responsible for routing URLs to relevant views [13].

(4) wsgi.py: "Web Server Gateway Interface (WSGI) is a mediator responsible for conveying communication between a web server and a Python web application" [14].

(5) manage.py: A command-line utility to interact with Django [15].

(6) passwordmanager (Django app)

  • admin.py: To display models in the Django admin panel [16].
  • apps.py: A configuration file common to all Django apps [17].
  • migrations: Contains app's migrations related files.
  • models.py: To define database for the app.
  • tests.py: For testing the app.
  • views.py: Contains Django views; which are Python functions that takes http requests and returns http response, like HTML documents [18].

Initial migration

"Migrations progate changes to models (adding a field, deleting a model, etc.) into the database schema" [19]. Let us do an initial migration

(venv) $ python3 manage.py makemigrations
(venv) $ python3 manage.py migrate

Output:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

Note: As soon as you make initial migration, db.sqlite3 appears on your project folder. By DB sqlite3 will be used as Django database.
You can use tools like SQLite Viewer for VSCode or DB Browser for SQLite to work with the database.

DB Browser for SQLite

figure 2. DB Browser for SQLite.

SQLite Viewer

figure 3. VS Code extension called SQLite Viewer.

createsuperuser

Now, create an initial superuser account. A superuser is an account that has all permissions [20].

(venv) $ python3 manage.py createsuperuser

Username: amaharjan  #replace with your admin username
Email address: hello@amaharjan.de  #replace with your email
Password: 
Password (again): 
Superuser created successfully.

runserver

(venv) $ python3 manage.py runserver

This will start the development server for the project at port 8000 (default port).
Browse http://127.0.0.1:8000/

Initial index page of the Django

figure 4. Initial index page of the Django.

For admin page, go to http://127.0.0.1:8000/admin/

django admin login form

figure 5. Django admin login page.

Django app

Continuing from the "Django setup" section. This section will be focused on the Django app; let's recap the folder structure for passwordmanager app:

....
├── passwordmanager
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   ├── models.py
│   ├── tests.py
│   └── views.py
....

models

Create a database model for passwordmanager. Paste the following code:

#models.py

1  from django.db import models
2  from django.urls import reverse
3
4  class PasswordManager(models.Model):
5      name = models.CharField(max_length=200)
6      username = models.TextField()
7      password = models.TextField()
8      url = models.TextField()
9      datestamp = models.DateTimeField(auto_now_add=True)
10
11     def get_absolute_url(self):
12         return reverse('password_manager_list')

Here, a Django model named PasswordManager represents data structure for the passwordmanager app. The database table will be created once the migration process is completed (which we'll cover later).

In line 2 reverse function is used to generate URLs by reversing the URL patterns (will be defined later in urls.py).

In line 4, we defined a Django model named PasswordManager.

From line 5 to 9, we defined fields for the model.
And in line 11, we defined the method called get_absolute_url; often used for redirecting after successful form submissions.

views

#views.py

from django.views.generic import ListView, DetailView, CreateView
from django.views.generic.edit import UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import PasswordManager

class PasswordManagerCreateView(CreateView):
    model = PasswordManager
    template_name = 'passwordmanager/password_add.html'
    fields = '__all__'

class PasswordManagerListView(ListView):
    model = PasswordManager
    template_name = 'passwordmanager/password_list.html'

class PasswordManagerUpdateView(UpdateView):
    model = PasswordManager
    template_name = 'passwordmanager/password_update.html'
    fields = '__all__'

class PasswordManagerDeleteView(DeleteView):
    model = PasswordManager
    template_name = 'passwordmanager/password_delete.html'
    success_url = reverse_lazy('password_manager_list')

class PasswordManagerDetailView(DetailView):
    model = PasswordManager
    template_name = 'passwordmanager/password_detail.html'

Django views in the django.views.generic module were utilized for performing CRUD operations, including:

  1. PasswordManagerCreateView:
    • View for creating a new PasswordManager object.
    • Response will be rendered by the password_add.html template.
  2. PasswordManagerDetailView:
    • Will render a "detail" view of the object.
  3. PasswordManagerListView:
    • Will render list of PasswordManager objects.
  4. PasswordManagerUpdateView:
    • View for updating the object.
    • A response will be rendered by the password_update.html template.
  5. PasswordManagerDeleteView:
    • View for deleting the object retrieved with self.get_object() (i.e., PK or ID of the PasswordManager object).
  6. model = PasswordManager:
    • means, we will be using PasswordManager model defined in models.py
  7. reverse_lazy('password_manager_list'):
    • reverse_lazy is a utility in Django used to convert URL names into corresponding URL paths [21].

Working with Django templates

HTML/CSS template

For simplicity I'll be using Bootstrap v5.0.2 for this project.
I've already created the template, so just copy the theme folder from https://github.com/asis2016/easy-password-manager/tree/main/theme onto your easy-password-manager folder. The folder structure should look like this

.
└── easy-password-manager
    └── theme
        ├── bootstrap.min.css
        ├── create.html
        ├── delete.html
        ├── detail.html
        ├── index.html
        ├── login.html
        ├── style.css
        └── update.html

Django employs templates to serve HTML content through a view. By default, it searches for templates within each app [22].
Create the following directories

(venv) $ cd passwordmanager
(venv) $ mkdir templates && cd templates
(venv) $ mkdir passwordmanager

The template structure for the app will look like this

.
├── passwordmanager
    └── templates
        └── passwordmanager
                ├── base.html
                ├── password_add.html
                ├── password_delete.html
                ├── password_detail.html
                ├── password_list.html
                └── password_update.html

base.html

Create the base.html with the following code:

{% load static %} <!-- (1) -->
<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="author" content="www.amaharjan.de">
    <title>Easy Password Manager</title>
    <!-- CSS -->
    <link href="{% static 'bootstrap.min.css' %}" rel="stylesheet">  <!-- (2) -->
    <link href="{% static 'style.css' %}" rel="stylesheet">  <!-- (3) -->

    <!-- Include Clipboard.js from CDN -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.min.js"></script>
</head>

<body>
    <nav class="container py-2 bg-light border-bottom">
        <div class=" d-flex flex-wrap">
            <ul class="nav me-auto">
                <li class="nav-item"><a href="#" class="nav-link link-dark px-2">Settings</a></li>
                <li class="nav-item"><a href="https://github.com/asis2016/easy-password-manager"
                        class="nav-link link-dark px-2">GitHub</a></li>
                <li class="nav-item"><a href="#" class="nav-link link-dark px-2">About</a></li>
            </ul>
            <!-- start logout link  -->
            <!-- ends logout link  -->
        </div>
    </nav>
    <header class="container py-3 mb-4 border-bottom">
        <div class="d-flex flex-wrap justify-content-center">
            <a href="{% url 'password_manager_list' %}"
                class="d-flex align-items-center mb-3 mb-lg-0 me-lg-auto text-dark text-decoration-none">
                <h5>Easy pass manager</h5>
            </a>
        </div>
    </header>
    <!-- body content -->
    <div class="container">
        {% block content %} <!-- (4) -->
        {% endblock content %}
    </div>

    {% block footer %} <!-- (5) -->
    {% endblock footer %}
</body>

</html>

base.html serves as a foundational template for other .html pages in the project. Let's break down the Jinja expressions used within this html:

(1) {% load static %}: This is used to include static files like CSS, JavaScript, images, etc.

(2) {% static 'bootstrap.min.css' %}: Dynamically generates the URL for the bootstrap.min.css location.

(3) {% static 'style.css' %}: Dynamically generates the URL for the style.css location.

(4) {% block content %} ... {% endblock content %}: These are block tags called content which will be overridden by the child template.

(5) {% block footer %} ... {% endblock footer %}: These are block tags called footer which will be overridden by the child template.

password_add.html

Create the password_add.html with the following code:

{% extends 'passwordmanager/base.html' %}   <!-- (1) -->

{% block content %}
<div class="row">
    <div class="col-md-6 offset-md-3">
        <form method="post">            <!-- (2) -->
            {% csrf_token %}            <!-- (3) -->
            <div class="card">
                <div class="card-header">
                    <h5>Add new credential</h5>
                </div>
                <div class="card-body">
                    <div class="mb-3">
                        <label for="id_name" class="form-label">Title</label>
                        <!-- (4) -->
                        <input type="text" class="form-control" id="id_name" name="name" maxlength="200" required="" autocomplete="off">
                    </div>
                    <div class="mb-3">
                        <label for="id_username" class="form-label">Username</label>
                        <input type="text" class="form-control" id="id_username" name="username" required="" autocomplete="off">
                    </div>
                    <div class="mb-3">
                        <label for="id_password" class="form-label">Password</label>
                        <input type="text" class="form-control" id="id_password" name="password" required="" autocomplete="off">
                    </div>
                    <div class="mb-3">
                        <label for="id_url" class="form-label">URL</label>
                        <input type="text" class="form-control" id="id_url" name="url" required="" autocomplete="off">
                    </div>
                </div>
                <div class="card-footer">
                    <div class="d-flex" style="justify-content: space-between; align-items: center;">
                        <button type="submit" class="btn btn-primary">Save</button>
                        <a href="{% url 'password_manager_list' %}">Go back</button> <!-- (4) -->
                    </div>
                </div>
            </div>
        </form>
    </div>
</div>
{% endblock content %}

password_add.html serves as the template for PasswordManagerCreateView (views.py). Let's break down the Jinja expressions used in password_add.html:

(1) {% extends 'passwordmanager/base.html' %}: This template extends from the base template.

(2) <form method="post">{% csrf_token %} ...: The form is the part of POST method which include Cross-Site Request Forgery (CSRF) token within.

(3) {% url 'password_manager_list' %}: Jinja tag to generate the URL for the PasswordManagerListView view.

password_delete.html

Create the password_delete.html with the following code:

{% extends 'passwordmanager/base.html' %} 

{% block content %}
<div class="row">
    <div class="col-md-6 offset-md-3">
        <form method="post">
            {% csrf_token %}
            <div class="card">
                <div class="card-header">
                    <h4 class="text-danger">Are you sure?</h4>
                </div>
                <div class="card-body">
                    <p class="text-danger">You are about to delete</p>
                    <table class="table">
                        <tr>
                            <td>Title</td>
                            <td>{{ object.name }}</td>    <!-- (1) -->
                        </tr>
                        <tr>
                            <td>Username</td>
                            <td>{{ object.username }}</td>    <!-- (3) -->
                        </tr>
                        <tr>
                            <td>Password</td>
                            <td>{{ object.password }}</td>    <!-- (3) -->
                        </tr>
                        <tr>
                            <td>URL</td>
                            <td>{{ object.url }}</td>    <!-- (4) -->
                        </tr>
                    </table>
                </div>
                <div class="card-footer">
                    <div class="d-flex" style="justify-content: space-between; align-items: center;">
                        <button type="submit" class="btn btn-warning">Yes, I'm sure</button>
                        <a href="{% url 'password_manager_list' %}">No, take me back</button>
                    </div>
                </div>
            </div>
        </form>
    </div>
</div>
{% endblock content %}

password_delete.html serves as the template for PasswordManagerDeleteView (views.py). Let's break down the Jinja expressions used in password_delete.html:

(1) {{ object.name }}: Retrieves PasswordManager object's name.

(2) {{ object.username }}: Retrieves PasswordManager object's username.

(3) {{ object.password }}: Retrieves PasswordManager object's password.

(4) {{ object.url }}: Retrieves PasswordManager object's url.

password_detail.html

Create the password_detail.html with the following code:

{% extends 'passwordmanager/base.html' %}

{% block content %}
<div class="row mb-4">
    <div class="col d-flex justify-content-end">
        <a href="{% url 'password_manager_list' %}" class="btn btn-primary btn-sm">
            < Go back to the list</a>
    </div>
</div>
<div class="row">
    <div class="col-md-6 offset-md-3">
        <table class="table">
            <tr>
                <th>Id</th>
                <td>{{ object.id }}</td>
            </tr>
            <tr>
                <th>Title</th>
                <td>{{ object.name }}</td>
            </tr>
            <tr>
                <th>Username</th>
                <td>{{ object.username }}</td>
            </tr>
            <tr>
                <th>Password</th>
                <td>{{ object.password }}</td>
            </tr>
            <tr>
                <th>URL</th>
                <td>{{ object.url }}</td>
            </tr>
            <tr>
                <th>Created date</th>
                <td>{{ object.datestamp }}</td>
            </tr>
        </table>
    </div>
</div>
{% endblock content %}

password_detail.html serves as the template for PasswordManagerDetailView (views.py).

password_list.html

Create the password_list.html with the following code:

{% extends 'passwordmanager/base.html' %}

{% block content %}
<div class="row mb-4">
    <div class="col">
        {% if user.is_authenticated %}          <!-- (1) -->
        <h6>{{ user }}'s Primary Vault</h6>
        {% endif %}
    </div>
    <div class="col d-flex justify-content-end">
        <a href="{% url 'password_manager_add' %}" class="btn btn-primary btn-sm">+ Add new credential</a>
    </div>
</div>
<div class="row">
    <div class="col">
        {% if object_list %}
        <table class="table">
            <thead>
                <tr>
                    <th></th>
                    <th>Username</th>
                    <th>Password</th>
                    <th>URL</th>
                    <th></th>
                </tr>
                {% for post in object_list %}       <!-- (2) -->
                <tr>
                    <td>{{ post.name }}</td>
                    <td>{{ post.username }}</td>
                    <td class="d-none">
                        <input type="text" class="form-control" id="validationDefault02" placeholder="Last name"
                            value="Otto" disabled>
                    </td>
                    <td>
                        <span id="passwordForPost{{ post.id }}">{{ post.password }}</span>
                        <span class="badge copyToClipboard" id="cpPwdBtnForPost{{ post.id }}"
                            data-clipboard-target="#passwordForPost{{ post.id }}">
                            &#128203;
                        </span>
                    </td>
                    <td>
                        <a href="{{ post.url|lower }}" target="_blank">{{ post.url|lower }}</a>
                    </td>
                    <th class="d-flex justify-content-end">
                        <a href="{% url 'password_manager_detail' post.pk %}"
                            class="btn btn-info btn-sm">Detail</a> 
                        <a href="{% url 'password_manager_update' post.pk %}"
                            class="btn btn-warning btn-sm">Update</a> 
                        <a href="{% url 'password_manager_delete' post.pk %}" class="btn btn-danger btn-sm">Delete</a>
                    </th>
                </tr>
                {% endfor %}
            </thead>
        </table>
        {% else %}
        <div class="alert alert-warning">No records found!</div>
        {% endif %}
    </div>
</div>
{% endblock content %}

{% block footer %}
<script>
    {% for post in object_list %}
    new ClipboardJS('#cpPwdBtnForPost{{ post.id }}');
    {% endfor %}
</script>
{% endblock footer %}

password_list.html serves as the template for PasswordManagerListView (views.py). Let's break down the Jinja expressions used in password_list.html:

(1) {% if user.is_authenticated %} ... It checks whether the user is authenticated. If so, will render content what is inside of it.

(2) {% for post in object_list %}... For loop over the list of objects (object_list), where each object represents PasswordManager.

(3) {% block footer %}... Contains a script that uses the ClipboardJS library [23] to enable copying passwords to the clipboard.

password_update.html

Create the password_update.html with the following code:

{% extends 'passwordmanager/base.html' %}

{% block content %}
<div class="row">
    <div class="col-md-6 offset-md-3">
        <form method="post">
            {% csrf_token %}
            <div class="card">
                <div class="card-header">
                    <h5>Edit {{ object.name }} credential</h5>
                </div>
                <div class="card-body">
                    <div class="mb-3">
                        <label for="id_name" class="form-label">Title</label>
                        <!-- (1) -->
                        <input type="text" class="form-control" id="id_name" name="name" maxlength="200" required="" value="{{ object.name }}">
                    </div>
                    <div class="mb-3">
                        <label for="id_username" class="form-label">Username</label>
                        <input type="text" class="form-control" id="id_username" name="username" required="" value="{{ object.username }}">
                    </div>
                    <div class="mb-3">
                        <label for="id_password" class="form-label">Password</label>
                        <input type="text" class="form-control" id="id_password" name="password" required="" value="{{ object.password }}">
                    </div>
                    <div class="mb-3">
                        <label for="id_url" class="form-label">URL</label>
                        <input type="text" class="form-control" id="id_url" name="url" required="" value="{{ object.url }}">
                    </div>
                </div>
                <div class="card-footer">
                    <div class="d-flex" style="justify-content: space-between; align-items: center;">
                        <button type="submit" class="btn btn-warning">Update</button>
                        <a href="{% url 'password_manager_list' %}">No, take me back</button>
                    </div>
                </div>
            </div>
        </form>
    </div>
</div>
{% endblock content %}

password_update.html serves as the template for PasswordManagerUpdateView (views.py). Let's break down the Jinja expressions:

... value="{{ object.name }}"> retrieves PasswordManager object's name and puts into value attribute of input tag.

Same goes for {{ object.username }}, {{ object.password }} and {{ object.url }}

URLs

The next step is configure URLs. Update easy_password_manager/urls.py with the following code:

#easy_password_manager/urls.py

 1 | from django.contrib import admin
 2 | from django.urls import path, include
 3 |
 4 | from passwordmanager.views import (
 5 |     PasswordManagerListView, 
 6 |     PasswordManagerCreateView, 
 7 |     PasswordManagerDetailView, 
 8 |     PasswordManagerUpdateView, 
 9 |     PasswordManagerDeleteView
10 | )
11 |
12 | urlpatterns = [
13 |     path('admin/', admin.site.urls),
14 |     path('<int:pk>/', PasswordManagerDetailView.as_view(), name='password_manager_detail'),
15 |     path('<int:pk>/edit/', PasswordManagerUpdateView.as_view(), name='password_manager_update'),
16 |     path('<int:pk>/delete/', PasswordManagerDeleteView.as_view(), name='password_manager_delete'),
17 |     path('create', PasswordManagerCreateView.as_view(), name='password_manager_add'),
18 |     path('', PasswordManagerListView.as_view(), name='password_manager_list'),
19 | ]

From line 4 to 10, we are importing all the views from views.py.

On line 12, we define urlpatterns for those views. For e.g., path('<int:pk>/', PasswordManagerDetailView.as_view(), name='password_manager_detail'),

The url path will look like http://127.0.0.1/100

name= can be used in Jinja expression, for e.g., <a href="{% url 'password_manager_detail' %}"...

Note:
No need to make any changes to other files, i.e., admin.py, apps.py, __init__.py, migrations, we can happily skip those for this tutorial.

Static directory

Futhermore, create a folder name static, to put all required .css, .js, etc

(venv) $ pwd
.../easy-password-manager
(venv) $ mkdir static && cd static      #Must be in project's root

Now,

(1) Copy theme/bootstrap.min.css into static directory.

(2) Copy theme/style.css into static directory.

Static directory tree

.
├── easy_password_manager
│   ...
├── passwordmanager
│   ...
├── static  <-- new
│   ├── bootstrap.min.css  <-- new
│   └── style.css  <-- new
└── ...

Settings

Update settings.py as mentioned below:

...
from pathlib import Path
import os  #new

...
INSTALLED_APPS = [
    ...
    'django.contrib.staticfiles',
    'passwordmanager.apps.PasswordmanagerConfig',  #new
]

...
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],  #new
        ....
    },
]

...
STATIC_URL = 'static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]  #new
...

Second migration

This time we will make migrations for the second time to the passwordmanager app.
First, stop the server by ^C (ctrl + c). Then do the following:

(venv) $ pwd
.../easy-password-manager
(venv) $ python3 manage.py makemigrations

Migrations for 'passwordmanager':
  passwordmanager/migrations/0001_initial.py
    - Create model PasswordManager
(venv) $ python3 manage.py migrate

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, passwordmanager,
  sessions
Running migrations:
  Applying passwordmanager.0001_initial... OK

Django table structure

figure 6. passwordmanager_passwordmanager table created as a result of second migration (software used: DB Browser for SQLite).

Run the server and browse http://127.0.0.1:8000/

(venv) $ python3 manage.py runserver

Django homepage

figure 7. Browsing http://127.0.0.1:8000/

Stop the server and update git before moving on to the next section

(venv) $ git add .
(venv) $ git commit -m "authorization added"

Authorization

"Authorization is the process of determining exactly what an authenticated party can do" [7]. In Django, a simple way to limit access to unauthenticated users is by using the LoginRequiredMixin [8]. Let's go ahead and make some updates to the code base.

Update views

First, stop the server by pressing ^c (ctrl + c). And update views.py as mentioned below:

from django.contrib.auth import logout  #new
from django.shortcuts import redirect  #new
from django.contrib.auth.mixins import LoginRequiredMixin  #new
from django.views.generic import ListView, DetailView, CreateView
from django.views.generic.edit import UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import PasswordManager

class PasswordManagerCreateView(LoginRequiredMixin, CreateView):    #update
    model = PasswordManager
    template_name = 'passwordmanager/password_add.html'
    fields = '__all__'
    login_url = 'login'  #new

class PasswordManagerListView(LoginRequiredMixin, ListView):        #update
    model = PasswordManager
    template_name = 'passwordmanager/password_list.html'
    login_url = 'login'  #new

class PasswordManagerUpdateView(LoginRequiredMixin, UpdateView):    #update
    model = PasswordManager
    template_name = 'passwordmanager/password_update.html'
    fields = '__all__'
    login_url = 'login'  #new

class PasswordManagerDeleteView(LoginRequiredMixin, DeleteView):    #update
    model = PasswordManager
    template_name = 'passwordmanager/password_delete.html'
    success_url = reverse_lazy('password_manager_list')
    login_url = 'login'  #new

class PasswordManagerDetailView(LoginRequiredMixin, DetailView):    #update
    model = PasswordManager
    template_name = 'passwordmanager/password_detail.html'
    login_url = 'login'  #new

def password_manager_logout(request):  #new
    logout(request)  #new
    return redirect('login')  #new

Explanation:

(1) All view inherits LoginRequiredMixin to ensure only authenticated users can access those views.

(2) password_manager_logout function is created that logs out the current user.

(3) login_url = 'login' redirects unauthenticated user to login page.

Update URLs

Update urls.py

from django.contrib import admin
from django.urls import path, include

from passwordmanager.views import (
    PasswordManagerListView, 
    PasswordManagerCreateView, 
    PasswordManagerDetailView, 
    PasswordManagerUpdateView, 
    PasswordManagerDeleteView,
    password_manager_logout  #new
)

urlpatterns = [
    path('accounts/', include('django.contrib.auth.urls')),         #new
    path('logout/', password_manager_logout, name='pm_logout'),     #new
    path('admin/', admin.site.urls),
    path('<int:pk>/', PasswordManagerDetailView.as_view(), name='password_manager_detail'),
    path('<int:pk>/edit/', PasswordManagerUpdateView.as_view(), name='password_manager_update'),
    path('<int:pk>/delete/', PasswordManagerDeleteView.as_view(), name='password_manager_delete'),
    path('create', PasswordManagerCreateView.as_view(), name='password_manager_add'),
    path('', PasswordManagerListView.as_view(), name='password_manager_list'),
]

Add login.html

By default Django looks into login.html from templates/registration folder which needs to be in root folder

.
├── easy_password_manager
├── passwordmanager
├── static
├── templates  <-- new
│       └── registration  <-- new
│               └── login.html  <-- new
└── ...
(venv) $ mkdir templates && cd templates
(venv) $ mkdir registration && cd registration
(venv) $ touch login.html

Add the following html template to login.html

1  {% load static %}
2  <!doctype html>
3  <html lang="en">
4  
5  <head>
6      <meta charset="utf-8">
7      <meta name="viewport" content="width=device-width, initial-scale=1">
8      <meta name="author" content="www.amaharjan.de">
9      <title>Easy Password Manager</title>
10  
11     <!-- CSS -->
12     <link href="{% static 'bootstrap.min.css' %}" rel="stylesheet">
13     <link href="{% static 'style.css' %}" rel="stylesheet">
14  </head>
15  
16  <body id="loginPage" class="text-center">
17      <main>
18          <form method="post">
19              {% csrf_token %}
20              
21              <!-- Errors -->
22              {% if form.errors %}
23              <div class="alert alert-danger errors pt-2">
24                  {{ form.errors }}
25              </div>
26              {% endif %}
27  
28              <h1 class="h3 mb-3 fw-normal">
29                  Sign in
30              </h1>
31              <div class="form-floating">
32                  <input class="form-control" type="text" name="username" autocomplete="off" autofocus="" autocapitalize="none"
33                      maxlength="150" required="" id="id_username" placeholder="amaharjan">
34                  <label for="id_username">Username</label>
35              </div>
36              <div class="form-floating">
37                  <input class="form-control" type="password" name="password" required="" id="id_password">
38                  <label for="id_password">Password</label>
39              </div>
40              <button type="submit" class="w-100 btn btn-lg btn-primary">Sign in</button>
41              <p class="mt-5 mb-3 text-muted">© www.amaharjan.de | 2023</p>
42          </form>
43      </main>
44  </body>
45  </html>

Here,

In line 1, we are loading static contents for the login.html.
In line 99, CSRF token must be used.
In line 22, if there is an error within the login form, it will display here.

Update base.html

Update base.html (starting from line 27) with the following piece of code:

...
27 <!-- start logout link  -->
28  {% if user.is_authenticated %}
29      <ul class="nav">
30         <li class="nav-item">
31              <a href="{% url 'pm_logout' %}" class="nav-link link-dark px-2">Logout</a>
32          </li>
33      </ul>
34  {% endif %}
35 <!-- ends logout link  -->
...

Here, the Jinja expression {% if user.is_authenticated %} will display "Logout"; only if the user is authenticated.

Update settings

Finally, make changes to the settings.py at the bottom of the page:

LOGIN_REDIRECT_URL = 'password_manager_list'
LOGOUT_REDIRECT_URL = 'login'

Once again, run the server and browse http://127.0.0.1:8000/

(venv) $ python3 manage.py runserver

You should be able to see that the unauthenticated user will be redirect to the login page.

admin login page

figure 8. Django project login page.

Stop the server and update git before moving on to the next section

(venv) $ git add .
(venv) $ git commit -m "authorization added"

Testing

"Code without tests is broken by design" [24]. So, in this final chapter, let's explore some test cases for the project. We'll name this set of tests PasswordManagerTest, and it will include:

(1) Ensuring that the data saved in PasswordManager matches the expected values.

(2) Verifying that all views response codes are "okay".

from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from .models import PasswordManager

class PasswordManagerTest(TestCase):

    #Setup initial data for the test
    def setUp(self):

        self.user = get_user_model().objects.create_user(    # <--Setup `testuser`
            username='testuser',
            email='test@example.com',
            password='password'
        )

        self.pm = PasswordManager.objects.create(    # <--Creates a `PasswordManager` object
            name='gmail',
            username='testuser@gmail.com',
            password='password',
            url='gmail.com'
        )

    def test_password_manager_content(self):
        '''
        Assert if the data saved in PasswordManager matches.
        '''
        self.assertEqual(f'{self.pm.name}', 'gmail')
        self.assertEqual(f'{self.pm.username}', 'testuser@gmail.com')
        self.assertEqual(f'{self.pm.password}', 'password')
        self.assertEqual(f'{self.pm.url}', 'gmail.com')

    def test_password_manager_list_view(self):
        '''
        Assert if the `PasswordManagerListView` is 200 OK.
        '''
        self.client.login(username='testuser', password='password')    # <--To login the user
        response = self.client.get(reverse('password_manager_list'))
        self.assertEqual(response.status_code, 200)

    def test_password_manager_detail_view(self):
        '''
        Assert if the `PasswordManagerDetailView` is 200 OK.
        '''
        self.client.login(username='testuser', password='password')    
        response = self.client.get('/1/')
        self.assertEqual(response.status_code, 200)

    def test_password_manager_delete_view(self):
        '''
        Assert if the `PasswordManagerDeleteView` is 302.
        '''
        self.client.login(username='testuser', password='password')
        response = self.client.post('/1/delete/')
        self.assertEqual(response.status_code, 302)

    def test_password_manager_update_view(self):
        '''
        Assert if the `PasswordManagerUpdateView` is 200 or 302.
        '''
        self.client.login(username='testuser', password='password')
        update_url = reverse('password_manager_update', kwargs={'pk': self.pm.pk})
        response_get = self.client.get(update_url)
        self.assertEqual(response_get.status_code, 200)

        updated_data = {
            'username':'newuser'
        }

        response_post = self.client.post(update_url, data=updated_data)
        self.assertIn(response_post.status_code, [200, 302])
        self.pm.refresh_from_db()

Line 10: The setUp method initializes initial data for the test.

Line 18: PasswordManager.objects.create creates a PasswordManager database object.

Line 26: test_password_manager_content() asserts if the saved data in PasswordManager matches.

Line 36: test_password_manager_list_view(), a test case for PasswordManagerListView.

Line 45: test_password_manager_detail_view(), a test case for PasswordManagerDetailView.

Line 54: test_password_manager_delete_view(), a test case for PasswordManagerDeleteView.

Line 64: test_password_manager_update_view(), a test case for PasswordManagerUpdateView.

Finally, run the testcases with the following command

(venv) $ python3 manage.py test

Result

Found 5 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 2.064s

OK
Destroying test database for alias 'default'...

Overall project structure

The overall project folder and file structure now looks like this

.
└── easy-password-manager
    ├── db.sqlite3
    ├── easy_password_manager
    │   ├── asgi.py
    │   ├── __init__.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    ├── LICENSE
    ├── manage.py
    ├── passwordmanager
    │   ├── apps.py
    │   ├── __init__.py
    │   ├── migrations
    │   │   ├── 0001_initial.py
    │   │   └── __init__.py
    │   ├── models.py
    │   ├── templates
    │   │   └── passwordmanager
    │   │       ├── base.html
    │   │       ├── password_add.html
    │   │       ├── password_delete.html
    │   │       ├── password_detail.html
    │   │       ├── password_list.html
    │   │       └── password_update.html
    │   ├── tests.py
    │   └── views.py
    ├── README.md
    ├── requirements.txt
    ├── static
    │   ├── bootstrap.min.css
    │   └── style.css
    ├── templates
    │   └── registration
    │       └── login.html
    └── theme
        ├── bootstrap.min.css
        ├── create.html
        ├── delete.html
        ├── detail.html
        ├── index.html
        ├── login.html
        ├── style.css
        └── update.html

11 directories, 34 files

Moving further

Awesome, you've just created a Django web project successfully. If you have any question or feedback, feel free to reach out to me at hello@amaharjan.com.

Happy coding!

Some of the best sources, in my humble opinion, for learning more about Django and Python include:

  1. docs[.]djangoproject[.]com ↗
  2. Django for Beginners by William S. Vincent ↗
  3. Python Crash Course, 2nd Edition ↗
  4. Two Scoops of Django 3.x ↗

Source code

https://github.com/asis2016/easy-password-manager

References