Django for Beginners #3

You can download the source code for this tutorial here.

Now, let’s talk about templates. The template layer is the frontend part of a Django application, which is why the template files are all HTML code since they are what you see in the browser, but things are slightly more complicated than that. If it contains only HTML codes, the entire website would be static, and that is not what we want. So the template would have to “tell” the view where to put the retrieved data.

Configuring the Django template system

Before we start, there is a something we need to change in the settings, we need to tell Django where we are putting the template files. Go to settings.py and find TEMPLATES.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            '/path/to/templates',
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Change the 'DIRS' option, which points to the template folder. Now, let’s verify that this setting works. Create a new URL pattern that points to a test() view.

1
path('test/', views.test),

Create the test() view.

1
2
def test(request):
    return render(request, 'test.html', {})

Create the templates folder and a test.html template.

django templates

1
<p>This is a template.</p>

Start the server and go to http://127.0.0.1:8000/.

This is a template

The Django template language

Displaying data

Recall that we can send data from the view to the template like this:

1
2
3
4
def test(request):
    return render(request, 'test.html', {
        'name': 'Eric'
    })

The string 'Eric' is assigned to the variable name and passed to the template. And inside the template, we can display the name using double curly braces, {{ }}.

1
<p>Hello, {{ name }}.</p>

Refresh the browser, and you will see the output.

Display Data

However, sometimes the data that is passed to the template is not a simple string. For example:

1
2
3
4
5
6
def test(request):
    post = Post.objects.get(pk=1)

    return render(request, 'test.html', {
        'post': post
    })

In this case, the post variable is in fact a dictionary (Dictionary is a data structure, if you are not familiar with it, consider reading this article). You can access the items in that dictionary like this in the template:

1
2
3
{{ post.title }}
{{ post.content }}
{{ post.pub_date }}

Filtering data

Filters transform the values of variables. For example, we have a variable django, with the value 'the web framework for perfectionists with deadlines'. If we put a title filter on this variable:

1
{{ django|title }}

The template will be rendered into:

The Web Framework For Perfectionists With Deadlines

Here is a full list of all built-in filters in Django.

Adding programming features to the Django template

Tags add programming language features such as flow control and loops to HTML code, which will save us a lot of time and resources, we don’t have to write the same code over and over again. All tags are defined using {% %}.

For example, here is a for loop:

1
2
3
4
5
<ul>
    {% for athlete in athlete_list %}
        <li>{{ athlete.name }}</li>
    {% endfor %}
</ul>

This is an if statement:

1
2
3
{% if somevar == "x" %}
  This appears if variable somevar equals the string "x"
{% endif %}

And this is an if-else statement:

1
2
3
4
5
6
7
{% if athlete_list %}
    Number of athletes: {{ athlete_list|length }}
{% elif athlete_in_locker_room_list %}
    Athletes should be out of the locker room soon!
{% else %}
    No athletes.
{% endif %}

Here is a full list of all built-in tags in Django. There are lots of other useful filters and tags in Django template system, we’ll talk about them as we encounter specific problems in the future.

The inheritance system of Django template language

The primary benefit of using the Django template is that we do not need to write the same code over and over again. For example, in a typical web application, we’ll usually have navigation bar and a footer, which will appear on every page. Repeating all of these code over and over again will make it very difficult for maintenance. Django offer us a very easy way to solve this problem.

Let’s create a layout.html file in the templates folder. As the name suggests, this is the place where we define the layout of our template. To make our example easier to read, I skipped the code for footer and navbar.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
    {% block meta %} {% endblock %}
    <!-- Import CSS here -->
</head>
<body>

<div class="container">
    <!-- Put the navbar here -->

    {% block content %} {% endblock %}

    <!-- Put the footer here -->
</div>
</body>
</html>

Notice that in this file, we defined two blocks, meta and content, using the {% block ... %} tag. To use this layout, we define a home.html template.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{% extends 'layout.html' %}

{% block meta %}
    <title>Page Title</title>
    <meta charset="UTF-8">
    <meta name="description" content="Free Web tutorials">
    <meta name="keywords" content="HTML, CSS, JavaScript">
    <meta name="author" content="John Doe">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
{% endblock %}

{% block content %}
<p>This is the content section.</p>
    {% include 'vendor/sidebar.html' %}
{% endblock %}

When this template is called, Django will first find the layout.html file, and fill out the meta and content blocks with information on this home.html page.

Also, notice there is something else in this template. {% include 'vendor/sidebar.html' %} tells Django to look for the /templates/vendor/sidebar.html and put it here.

1
<p>This is the sidebar.</p>

It is not exactly a sidebar, but we can use it to demonstrate this inheritance works. Make sure your view is correct.

1
2
3
4
from django.shortcuts import render

def home(request):
    return render(request, 'blog/home.html', {})

And make sure your URL dispatcher points to this view.

1
path('home/', views.home),

Creating the homepage

Starting from this section, we are going to start putting what we’ve learned about Django so far into real-life application. And to not overwhelm you, we’ll start with something easier. In this section, we are going to create only a homepage. This homepage is going to be very simple, we need a logo, a site name and a description. But through this example, you’ll see exactly how the model, view and template layer can work together.

As a quick reminder, this is how we can create a new Django project:

1
2
3
4
python -m pip install Django
django-admin startproject example
cd example
python manage.py startapp blog

Remember to make the necessary changes to the configuration file (settings.py) like we talked about in this article.

The model layer

Now, let’s start with the model layer. Since we are only building a home page, we only need one model to store all the data, let’s call it home. Inside this model, we are going to need three fields, title, description and logo.

1
2
3
4
5
6
from django.db import models

class Home(models.Model):
    title = models.CharField(max_length=255)
    description = models.TextField()
    logo = models.ImageField(upload_to='uploads/images/logo')

Notice that the logo is an ImageField. This field will create a column in the database, which stores the path that points to the location of the image, and the upload_to attribute defines that path. You’ll see exactly how this works later.

Also, you need to have the Pillow package installed to use the ImageField. Here is how you can install the package if you haven’t already done so.

1
2
python -m pip install --upgrade pip
python -m pip install --upgrade Pillow

Next, we can generate the migration files and apply them to our database.

1
2
python manage.py makemigrations
python manage.py migrate

This is what the database should look like. Notice that the logo column is of type VARCHAR, which means it should be a string.

database setup

View, template and URL

Next, it’s time to deal with views and templates. Let’s first ask ourselves a question, how many views and templates do we need to create a home page?

We mentioned before that in the field of web development, there is something called CRUD (Create, Read, Update and Delete) operation. Basically, we need to make sure that the user can browse the items in the database table, read the details of an item, create new items and store them in the database, update an item and of course, delete an item.

To create a complete CRUD operation, we’ll need at least seven view functions.

  • index() - Display a listing of items.
  • show(id) - Display the specified item.
  • create() - Show the form for creating a new item.
  • store(request) - Store a newly created item in database.
  • edit(id) - Show the form for editing the specified item.
  • update(request, id) - Update the specified item in database.
  • destroy(id) - Remove the specified item from database.

For our homepage, we don’t need index() since we are only going to have one item in the database, and we are not going to talk about edit() and update(), since they are basically the same as create() and store().

As for the template, we need a create.html page to display a form that we can use to create a new Home. A success.html page that tells us we’ve successfully created a new Home, or successfully deleted one. And finally, a home.html page that displays the homepage.

The create action

So, let’s start with the create page. We need a layout.html and a create.html file. I put all the templates in the templates directory. Remember to change the corresponding setting in the settings.py file.

Create page

layout.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />

    {% block meta %} {% endblock %}

    <!-- Bootstrap -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
</head>

<body>
    <!-- Responsive navbar-->
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <a class="navbar-brand" href="#">{{ title }}</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
                data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
                aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav ms-auto mb-2 mb-lg-0">
                    <li class="nav-item"><a class="nav-link active" aria-current="page" href="#">Home</a></li>
                    <li class="nav-item"><a class="nav-link" href="#">Link</a></li>
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" id="navbarDropdown" href="#" role="button"
                            data-bs-toggle="dropdown" aria-expanded="false">Dropdown</a>
                        <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
                            <li><a class="dropdown-item" href="#">Action</a></li>
                            <li><a class="dropdown-item" href="#">Another action</a></li>
                            <li>
                                <hr class="dropdown-divider" />
                            </li>
                            <li><a class="dropdown-item" href="#">Something else here</a></li>
                        </ul>
                    </li>
                </ul>
            </div>
        </div>
    </nav>
    <!-- Page content-->
    <div class="container">
        {% block content %} {% endblock %}
    </div>
    <!-- Bootstrap core JS-->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous">
    </script>
</body>

</html>

create.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{% extends 'layout.html' %}

{% block meta %}

<meta name="description" content="{{ title }}" />
<title>{{ title }}</title>

{% endblock %}

{% block content %}
<br>

<form action="/save/" method="POST" enctype="multipart/form-data">
    {% csrf_token %}
    <div class="mb-3">
        <label for="inputTitle" class="form-label">Website Title</label>
        <input type="text" class="form-control" id="inputTitle" name="title">
    </div>
    <div class="mb-3">
        <label for="inputDescription" class="form-label">Website Description</label>
        <textarea type="text" class="form-control" id="inputDescription" name="description"></textarea>
    </div>
    <div class="mb-3">
        <label for="inputLogo" class="form-label">Choose a logo</label>
        <input type="file" class="form-control" id="inputLogo" name="logo">
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

{% endblock %}

There are three things we need to talk about in this form.

First, line 13, the action attribute defines the URL that your browser will request when the Submit button is clicked. The method attribute defines the method of that HTTP request, which in our case, is POST. The enctype attribute defines how the data will be encrypted, since we are uploading an image file, multipart/form-data is our only option. If you are interested in other options, consider reading this article.

Second, line 17, 21 and 25. Notice that all of them have a name attribute. This is very important, we are going to use it to retrieve the corresponding user input, which we’ll talk about later.

And lastly, the button must have type submit, or the form won’t do anything.

Next, let’s create the corresponding view. We need a create() method that displays the create.html page.

1
2
3
4
5
6
7
8
from django.shortcuts import render
from blog.models import Home

def create(request):

    return render(request, 'create.html', {
        'title': "Create a new Home",
    })

The create() view is relatively easy. We only need to pass one title variable to the template.

The save action

For the save action, we need a success.html page that tells us the data has been saved.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{% extends 'layout.html' %}

{% block meta %}

<meta name="description" content="{{ description }}" />
<title>{{ title }}</title>

{% endblock %}

{% block content %}

<div class="text-center mt-5">
    <h1>Success!</h1>
</div>

{% endblock %}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def save(request):
    home = Home(
        title = request.POST.get('title', ''),
        description = request.POST.get('description', ''),
        logo = request.FILES['logo'],
    )

    home.save()

    return render(request, 'success.html', {
        'title': "Success",
        'description': "Successfully Saved"
    })

In the save() view, from line 2 to 6, we created a new instance of Home, and remember that our Home model should take three attributes, title, description and logo.

Line 3, request.POST.get('title', '') is how we can access the body of the HTTP POST request. The get() method takes two parameters, the first one 'title' matches the value of the name attribute in our form, which we just talked about. And the second parameter is the default value, if the user input is empty, the default value (empty string '') will be assigned.

Line 5, request.FILES['logo'] is how we can store the image file. Dealing with uploaded files is a little bit tricky in Django. We need to add something to the settings.py file.

1
2
3
4
import os

MEDIA_ROOT =  os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

This code will inform Django where to put the uploaded media files, and what URL to use when trying to access these files. In our case, the media files will be put inside the media directory.

Now, when you upload an image, that image file will be put into directory media/uploads/images/logo, as defined in our model. And the path that points to the image file will be stored in the database.

Remember to add the URL dispatchers:

1
2
3
4
5
6
7
8
from blog import views
from django.urls import path

urlpatterns = [
    ...
    path('create/', views.create),
    path('save/', views.save),
] 

You can test the create and save page by going to http://127.0.0.1:8000/create/.

upload image

upload database setup

The show action

The show page should be very simple for you now. First, we need a home.html file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{% extends 'layout.html' %}

{% block meta %}
<meta name="description" content="{{ description }}" />
<title>{{ title }}</title>
{% endblock %}

{% block content %}
<div class="text-center mt-5">
    <img src="{{ logo }}" width="200px">
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>

    <form action="/delete/" method="POST" enctype="multipart/form-data">
        {% csrf_token %}
        <button type="submit" class="btn btn-danger">Delete Home</button>
    </form>
    
</div>
{% endblock %}

The view function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def show(request):
    home  = Home.objects.first()

    title = home.title
    description = home.description
    logo = home.logo.url

    return render(request, 'home.html', {
        'title': title,
        'description': description,
        'logo': logo,
    })

Line 6, home.logo.url retrieves the URL that points to the images, so that we can use it directly in the template.

And don’t forget the URL config:

1
path('', views.show),

However, if you go to http://127.0.0.1:8000/, you will notice that the image file is not loading, but the link is pointing to the correct location. This is because by default, Django does not serve media files, to change this, we need to add another line in the url.py file.

1
2
3
4
5
6
7
8
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    ...
] 

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Now, refresh the browser, and everything should work fine. Remember that this is how we serve median files in the dev environment, in the production environment, we’ll have to do something different, which we’ll talk about in the future.

The delete action

Notice that in the home.html file, we have another form, which points to the URL /delete/. This leads us to the final part of this article, the delete view. To save us some time, we’ll use the same success.html file, but in a real application, you should probably use a different template.

1
2
3
4
5
6
7
8
9
def delete(request):
    home  = Home.objects.first()

    home.delete()

    return render(request, 'success.html', {
        'title': "Success",
        'description': "Successfully Deleted"
    })

When this view is executed, it will find the first record in the Home model, and delete it.


comments powered by Disqus