Learn Django | Crash Course

In this article, you will get a crash course to learn Django. Learn how to build a simple task manager.

I will begin with installation and setup, continue with explaining how things work and similar. After this, I will build an application. The most important part of this tutorial is to learn about how Django works and similar, and not the application it self.

The project we're going to build is a todo application. We will build it piece by piece, and I will introduce lots of important features of Django.

This article will be perfect for beginners and people who wants to dive into Django. I will explain things as well as I can, so if you've already tried Django but don't understand it, hopefully this series will be helpful to you.

You can install Django a couple of different ways. Directly on your computer, a virtual environment, a virtual machine, Docker, etc. So there are a lot of different ways to do this. In this tutorial, we're going to stick with a virtual environment. This is really easy to get started with, and it's one of the recommended ways to do it.

A virtual environment is a little environment in your computer. When it's activated, we can install Python packages (like Django) for this environment only. It also makes it easy to maintain and deploy, because we can replicated the environment on a server and similar.

Virtual environment

So first, we can install the virtual environment using pip (a package manager for Python). Run the following command in your command line/terminal.

$ pip install virtualenv

When that's finished, we can create a new environment for our project. Let's call it "toodoo_env".

$ virtualenv toodoo_env

Great, now we have a virtual environment. Let's go into it and activate it.

$ cd toodoo_env
$ source bin/activate

Now, the name of the environment should be infront of your username in the command line. As long as the environment is activated and we use "pip", the package will only be installed for the activated environment.

Django

Finally it's time to install Django and create an empty project.

$ pip install django

This will install the latest stable version of Django and a few dependencies that Django has. Next we will create the project.

$ django-admin startproject toodoo

The "django-admin" command was made available when we installed Django.

Now, we have an empty Django project and we're ready to start digging into the code.

Here is the list of files in our project:

manage.py
This file is a script for doing administrative tasks. It will be used many times during this series, and we will use it to create super users, create Django apps, interact with the database and similar. Right now, you don't have to think to much about it.

toodoo
Inside the project root folder, we get another folder named "toodoo". This is where all the configuration for the project will be placed. So it's kind of the center of the whole Django project.

toodoo->__init__.py
This file is empty, but it still has a function. It makes Python treat this folder as a module, so it's easy to reference the files inside it and similar.

toodoo->asgi.py
This file is a file the web server will be using (later). Asynchronous Server Gateway Interface. It's entry points for the webserver, and will only be used if the project supports asynchronous views (which we will not cover in this series).

toodoo->settings.py
This is where all the settings will be placed. This is where we define the database connection, where templates are located, security settings, how dates are formatted and similar.

toodoo->urls.py
This file is sort of like a table of contents. The webserver uses this file to find out what to show you when you visit a url.

toodoo->wsgi.py
This file is a file the web server will be using (later). We're not going to do anything with it, but we will use it later when we deploy the project to a live server.

This might be a little bit information overload, but I promise that everything will start making more sense when we start using the files.

What is an app

First of all, we have created a Django project called Toodoo. And a Django project usually exist of multiple apps. It's not necessarily easy to explain what a Django app is, so I will give you an example.

The project we're going to build in this series will be a task manager. It's not a very complicated project, so we will probably only have one app called "Task". The Task app will consist of a few files. We will have one file where we "describe" to the database what information we want to store there, this is called a "model". There will also be a file where we create views, a folder for the templates and similar. We will go through each of them when we have created the first one.

Let's say that we also had user profiles for the project. Then we would have one more app for that. It's not impossible to have everything in one really big app, but that's really not good practice.

Creating the app

To create the app, we need to run a command. Be sure to be in the same folder as the "manage.py" file.

$ python manage.py startapp task

And that's it! You have not created your first Django app. But even though we have created it, Django doesn't actually know that it exists. So we need to append it to a list in the "settings.py" file.

INSTALLED_APPS = [
...
'task',
]

As you can see, there are already a few apps in the list. These comes built into Django. It's apps for handling security, caching, messages and similar. Don't worry, we will come back to them later in this series.

So just append 'task' to the end of the list, and you're good to go :-)

What are the different files

__init__.py
This file is empty, but it still has a function. It makes Python treat this folder as a module, so it's easy to reference the files inside it and similar.

admin.py
Here, we can register database model with the build in admin interface that Django comes with. We can also add configurations to them, so we get search, filtering and similar.

apps.py
This file is for configurations for the app.

models.py
This is where we define what information we store in the database. For example that we want a title for the task, a status, a description and similar.

tests.py
In this file, we can write tests for the app. To make sure that everything is working as it should.

views.py
A view is used to get information from the database, and render it in a template.

Summary

Just like the files in the root folder, this might be a little bit information overload. But in the next part, we will finally start coding and I promise that everything will start making more sense then.

What is a view?

A view can be used for a variety of tasks. A typical view can either be to just render a simple template, you can get information from the database and render it in a template, and similar.

Essentially what happens is that Django loops through the urls.py file and try to find a path that matches the url your visiting. When Django finds a path, it will be connected to a view. And this view will get information, do things to the information or similar, and then render a template.

Function based views vs Class based views

There are two main different ways to create views with Django. Function based views and Class based views. For beginners, it's usually easier to understand Function based views at the beginning, so this is what I will stick to.

Don't worry, I will show you later in this series how to use class based views as well :-)

The front page view

Let me show you an example, and then explain what's going on (task/views.py):

from django.shortcuts import render

def frontpage(request):
return render(request, 'task/frontpage.html')

And there it is :-)

At line 1, we import a function from Django called "render". This is used to render a template and send information to it. Then at line 3, we define a view called frontpage. This is just a typical Python function. We pass in a parameter called "request", don't worry where it comes from. The request parameter contains information about the request like browser, ip-address, if it's post or get and much more. We will use it later in this series. Then at line 4, we use the render function where we first pass in the request parameter (this makes it available for use in the template). After that, we just specify where the template is located.

from django.shortcuts import render

def frontpage(request):
title = 'This is a variable'

return render(request, 'task/frontpage.html', {'title': title})

It's not so much more complicated, but I wanted to show how to send data to the template. First I define a variable called "title", this can be called whatever you want.

And then, on the render line, we have added a little dictionary at the end where we pass in the title.

Summary

So now that we have a basic view to use, we will procede to the template tomorrow :-)

A short introduction to templates

A template is usually just a simple HTML file. You use HTML just as you're used to, and then Django makes it possible to use different tags and blocks to make the templates dynamic. Django uses a template language very similar to twig, ninja and similar. So it's very easy to understand.

The front page template

Let's set up the template for the front page. This will make it easier to understand what's going on. First, create a new folder called "templates" in the "task" app folder, and then a folder called "task" inside the templates folder.

Django automatically tries to find a folder called "templates" in each of the app folders. And the reason why we have a new folder inside there called "task" is to make it easier to separate the apps later (You will understand when we get there).

Next, you can create a file called "frontpage.html" in the last task folder you created. It should look like this:

<div class="frontpage">
<h1>{{ title }}</h1>

<p>This is just a hard coded paragraph!</p>
</div>

That wasn't hard?

Most of this is just pure HTML. The only Django thing here is the h1 tag with curly braces inside. This is called a Django tag.

The "title" inside the two curly braces points to the "title" variable we passed in to the render function in the previous part of this series.

Summary

So now you've created your first Django template :-D
In the next part of this series, we will finally test the project and see it in action in a web browser.

The urls.py file

The project is almost ready for testing, we just need to add the front page view to the list of available urls.

It's best practice to have a separate urls.py file for each of the Django apps. But in this part I will only use the main urls.py file to keep things simple, and then we'll come back to fix it later.

Open up "toodoo/urls.py". First we need to import the front page view. So above the "urlpatterns" list, add this:

from task.views import frontpage

Next, we need to add it to the list of urls. So inside the urlpatterns list, add this (above the admin url):

path('', frontpage, name='frontpage'),

What happens here is that we use a function from Django called "path" to set up the url.

The first parameter "''" means that there is no url. So when we go to the domain or website address, this url will be triggered.
The second parameter "frontpage" is the view we want to call when we visit the url.
And the third parameter "name='frontpage'" is just a global name for this. This makes it easy to reference it in our project.

Nice! Now we can continue to the testing :-D

Running the webserver

For development purposes, Django comes with a built in webserver we can use while testing. To run this, you need to go to the command line and make sure you're in the same folder as the "manage.py" file.

When you're there, run this command.

$ python3 manage.py runserver

It will look something like this:

System check identified 12 issues (0 silenced).
April 27, 2022 - 04:27:06
Django version 4.0.3, using settings 'toodoo.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Now you should be able to go to 127.0.0.1:8000 in your browser and see the front page we have created!

The project isn't very impressive yet, but hang in there!

A few days ago, we created our first Django template. But it lacks many things. A valid HTML page at least needs the html-tag, the head-tag and the body-tag. But we don't want to keep writing these for all the templates we create. The solution to this is to extend templates.

In other words, we create a base template with the necessary html-tags. Plus, we add the menu there, the footer and similar.

Let's begin by making a new file called base.html inside the template folder (the same folder where frontpage.html is located). It should look like this:

<!doctype html>
<html>
<head>
    <title>Toodoo</title>
</head>

<body>
    <nav>
        The menu
    </nav>


    <footer>
        The footer
    </footer>
</body>
</html>

Okay, it's not very impressive yet... But anyways, let's make it possible for the frontpage.html to extend this template now. We want to make it possible to insert data between the nav and the footer. To do this, we add something called a tag-block:

{% block content %}
{% endblock %}

The name "content" is something that's typically used for the main content (title, texts, images and similar). We'll add more later.

Then, the next step is to make sure that the frontpage.html is using this template. The frontpage.html file should look like this:

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

{% block content %}
<div class="frontpage">
    <h1>{{ title }}</h1>

    <p>This is just a hard coded paragraph!</p>
</div>
{% endblock %}

The first change here is the "extends" tag. We just say to the rendering engine that we want to extend "this" html file and points to the base.html file. Below, we add the same block tag as in the base file to tell Django where to put the content inside.

If you open the website in your browser again, it should now look a little bit different. There should now be a "nav" and a "footer" as well. Now it is much easier to add more pages to our project. And if we want to change the menu, the only file we need to change is the base.html.

Django templates also has a lot of other cool features. Like functionality for looping, testing and so much more.

What is a database model?

A database model is used to describe to a database what information we want to store there, and also to make functionality to interact with it. It's probably easier to understand if I show you the database for the tasks first.

Database model for the tasks

Open up "task/models.py" and add the following there.

class Task(models.Model):
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True, null=True)
    is_done = models.BooleanField(default=False)

Okay, we don't need anything more there yet.

First, we create a new class with the name of the model and pass in "models.Model" to tell Django that we're using this class. Then, inside the class, we define three different fields to the model. As you can see, there are different field types (and there are of course many more as well).

The CharField is usually used for shorter strings like titles, names, addresses and similar. The TextField is used for longer texts. As you can see, we pass in "blank=True, null=True". This is to allow the field to be empty/optional. The title doesn't have this, so if you try to add a task without it, you will get an error.

For the last field, we use a BooleanField (True/False) to make it possible to track if a task is done or not. The default value will be set to False.

Updating the database

Everytime we create a new database model or make a change to it, we need to update the database. Don't worry this is just two simple commands.

$ python3 manage.py makemigrations

When you run this command, Django will create some new files for us. The files contains information about the database model we created. It's just a more low level Python script about the SQL. Next, we just need to execute the migration scripts.

$ python3 manage.py migrate

When this is done, a new table should have been created in the database and we're ready to continue.

Summary

For many people, the concepts of models/databases/migrations can be a little bit confusing. Do make it a little bit easier to understand, I skipped going into the migrations files for now. It's just not very important for beginners to understand (in my opinion).

We will work more with models and it's functionality later in this tutorial series.

Django comes with a brilliant admin interface. This can be used to add, edit, delete and view all of the data connected to our project. We can also add custom features and similar.

Let's say that we wanted to add a few tasks. We can use the shell or we can do this programmatically, but that's not very convenient right now. So let's create a superuser and log in.

Creating a superuser

Django comes with an authentication system. And this system has roles and permissions out of the box. So if you create a new user, that user will not automatically have access to the admin interface. We need permissions to do this. So in our case, we're going to create something called a superuser.

Let's go to the command line, and run this command:

$ python3 manage.py createsuperuser

Just follow the wizard and select a username, password and email. When that's done, we can run the webserver again.

$ python3 manage.py runserver

Btw, the admin interface is actually one of the other predefined INSTALLED_APPS inside the settings.py file. You can also see in the urls.py file that the admin interface has been added there.

Once you're logged in, you will see that we can see users and groups. If you click the users link, you will see the superuser you just created. If you want to do some changes to it, just give it a try :-)

Registering the task model

So where is the "task" model? Eventhough we registered our app in INSTALLED_APPS and similar, it doesn't automatically appear here in the admin interface. But luckily for us, it's very easy to do so.

Open up the file "task/admin.py" and add the following content to it:

from .models import Task

admin.site.register(Task)

First, we import the database model. We can use ".models" to refer to the models.py file since we're in the same folder. Next, we use a function from django to register the model.

If you go back to the admin interface and refresh now, you will see that the task model is there. Go ahead and add a few test tasks.

We will come back to the admin interface later and do some customizations here. We are going to add search, filters and some other cool things.

A few days ago, we created our first database model and it looked like this:

Class Task(models.Model):
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True, null=True)
    is_done = models.BooleanField(default=False)

So this is currently the information we have for our tasks. What we want to do in this part is to get the data from the database, and print it in the template (frontpage.html).

Let's begin by opening the task/views.py file. Right now, the file looks like this:

from django.shortcuts import render

def frontpage(request):
    title = 'This is a variable'

    return render(request, 'task/frontpage.html', {'title': title})

Here, we want to import the Task model and a few more things. Make these changes to the file:

from django.shortcuts import render

from .models import Task # New, 1

def frontpage(request):
    title = 'This is a variable'
    tasks = Task.objects.all() # New, 2

    return render(request, 'task/frontpage.html', {'title': title, 'tasks': tasks}) # Changed, 3

I have added comments on the lines that I changed.
1. Here we import the database model.
2. Here we define a variable called "tasks". Then we use the Task model to get all the objects (tasks).
3. On this line, we appended the tasks together with the title. So now, both of these will be available in the template.

Now that everything in the backend is done, we can do some changes to the template. Now it looks like this:

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

{% block content %}
    <div class="frontpage">
        <h1>{{ title }}</h1>

        <p>This is just a hard coded paragraph!</p>
    </div>
{% endblock %}

Then, below the paragraph, we want to loop through the tasks:

{% for task in tasks %}
    <div>
        <p>{{ task.title }}</p>
    </div>
{% endfor %}

So what we do here is that we use a for loop to iterate throught them. Accessing a property on a model is as easy as just saying "{{ task.title }}". We could also print the task.is_done, or task.description if we wanted that.

Let's do one more change to the backend, so we only get task with "is_done=True".

tasks = Task.objects.filter(is_done=False)

So instead of using ".all()", we now use ".filter()". Here we can pass in filters that Django should use for the query. Later in this series, we will go through more like this.

If you now go to the admin interface and set a task to "Done", it will no longer appear on the frontpage.

Just like we created the task app, we're now going to create an app for the categories.

$ python manage.py startapp category

Great, the app is now created and we can add it to the list of installed apps:

INSTALLED_APPS = [
    ...
    'category',
]

Since we now have the app, let's continue by creating the database model. Open up "category/models.py", and add the following content to it:

Class Category(models.Model):
    title = models.CharField(max_length=255)

Right now, I'm not going to add more fields to it. Let's update the database before we continue:

$ python3 manage.py makemigrations

When you run this command, Django will create some new files for us. The files contains information about the database model we created. It's just a more low level Python script about the SQL. Next, we just need to execute the migration scripts.

$ python3 manage.py migrate

When this is done, a new table (category) should have been created in the database and we're ready to continue.

Let's register this for the admin interface and add a few categories. Inside "category/admin.py", add the following content:

from .models import Category

admin.site.register(Category)

If you log in to the admin interface now, you will see that "Categorys" appears in the list. Categorys is definitely not the correct way to write this. But Django automatically pluralize the names, so how can we fix this?

Go back to "category/models.py" and make the following changes:

Class Category(models.Model):
    title = models.CharField(max_length=255)
    
    class Meta: # New, 1
        verbose_name_plural = 'Categories' # New, 2

1. To configure the model and some of its behaviour, we can add a Meta class.
2. To set the pluralized name, we add the "verbose_name_plural" property.

If you now go back and refresh, the correct name should be showing.

You've probably also noticed that when you go in to the list of categories, the names of the categories shows like this:
or similar. This is a named just based on the class name and doesn't tell us anything about which category it is.

To change this, we can set the class represenation as one of the methods for the database model. Open up category/models.py again, and below the meta class, add this:

def __str__(self):
    return self.title

What this does is essentially just take the title property, and override the build in __str__ method for the class.

Django ForeignKeys

There are a couple of ways to connect two database models. This is usaully done with a foreignkey field og a manytomany field. In this part, we we'll cover foreignkey. This perfect for when we want one category to have many tasks. It also makes it easy to find out which category a certain task belongs to.

Let's open up task/models.py and do some changes there:

from category.models import Category # New, 1

Class Task(models.Model):
    category = models.ForeignKey(Category, related_name='tasks', on_delete=models.CASCADE) # New, 2
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True, null=True)
    is_done = models.BooleanField(default=False)

1. First we import the category model we have created.
2. Here, we add a new field called category, which is a foreignkey. We pass in the category model and set something called a related name. This makes it really easy to get all the tasks which belongs to a category. The "on_delete=models.CASCADE" means that if we delete a category, we also want to delete all of the connected tasks.

Next step then is to update the database again:

$ python3 manage.py makemigrations

This is the same command as when we created the database models, but it's also used for updating them as well.

$ python3 manage.py migrate

When this is done, the task table in the database should be updated. NB NB NB:
You might be asked to set a default category or similar here, just type 1 on your keyboard.

Summary

So, now we have tasks and categories + they are connected to eachother. So tomorrow, we will continue by showing categories on the website and the tasks connected to it.

Getting the categories

The first thing we need to do here is to do some changes to the frontpage view. So open up task/views.py and make the following changes:

from django.shortcuts import render

from .models import Task
from category.models import Category # New, 1

def frontpage(request):
    title = 'This is a variable'
    tasks = Task.objects.all()
    categories = Category.objects.all() # New, 2

    return render(request, 'task/frontpage.html', {'title': title, 'tasks': tasks, 'categories': categories}) # Changed, 3

1. First, we import the category model.
2. Then, we get all of the categories from the database and assign it to a variable.
3. And to make it available for use in the template, we add it here in the dictionary.

Great, now we need to restructure the template a little bit. I know that it looks aweful, but it will be fixed a little bit later in the series. Open up the file called "frontpage.html" and make it look like this:

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

{% block content %}
    <div class="frontpage">
        <h1>{{ title }}</h1>

        <div class="columns">
            <div class="column is-8">
                <h2>Tasks</h2>

                {% for task in tasks %}
                    <div>
                        <p>{{ task.title }}</p>
                    </div>
                {% endfor %}
            </div>

            <div class="column is-4">
                <h2>Categories</h2>

                {% for category in categories %}
                    <div>
                        <p>{{ category.title }}</p>
                    </div>
                {% endfor %}
            </div>
        </div>
    </div>
{% endblock %}

So here is a few new changes. We added some more divs, so it's easier to separate the tasks from the categories. Don't worry about the class names (columns, columns, is-8) yet, we will come back to that.

If you go to the browser again now, you will see the tasks listed there, but also all of the categories.

Category detail view

I want to make it possible to go into the detail view of a category. Let's just begin with the template. Create a new folder called templates inside the category app, and a folder called templates inside that. In that folder, create a new file called "detail.html". It should look like this:

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

{% block content %}
    <div class="frontpage">
        <h1>{{ category.title }}</h1>

        <div class="columns">
            <div class="column is-8">
                <h2>Tasks</h2>

                {% for task in category.tasks.all %}
                    <div>
                        <p>{{ task.title }}</p>
                    </div>
                {% endfor %}
            </div>

            <div class="column is-4">
                <h2>Categories</h2>

                {% for category in categories %}
                    <div>
                        <p>{{ category.title }}</p>
                    </div>
                {% endfor %}
            </div>
        </div>
    </div>
{% endblock %}

As you can see, it's almost identical to the frontpage.html file. The only difference is the title, here we want to show the category title instead.

Next, open up "category/views.py" so we can create the view.

from django.shortcuts import render

from .models import Category

def category_detail(request, pk):
    category = Category.objects.get(pk=pk)
    categories = Category.objects.all()

    return render(request, 'category/detail.html', {'categories': categories, 'category': category})

This is also a little bit similar to the front page view. But there is one important difference. We have a new parameter in the function called "pk". This is short for primary key. This is an ID that we get from the URL, so we know which category to get from the database.

And when we get the category, we set "pk=pk". The first pk is the field in the database, and the second pk is the name in the parameter.

Okay, let's add this url to the urlpatterns.

Open up "toodoo/urls.py". First we need to import the category detail view. So above the "urlpatterns" list, add this:

from category.views import category_detail

Next, we need to add it to the list of urls. So inside the urlpatterns list, add this (below the front page url):

path('<int:pk>/', category_detail, name='category_detail'),

1. Here we add a dynamic value to the path. We say that we expect an integer (int) and give it the name of "pk" (the same name as in the view).
2. Then we pass in the view we want to use and set the name.

Last step before we can test now is to modify the frontpage template.

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

{% block content %}
    <div class="frontpage">
        <h1>{{ title }}</h1>

        <div class="columns">
            <div class="column is-8">
                <h2>Tasks</h2>

                {% for task in category.tasks.all %}
                    <div>
                        <p>{{ task.title }}</p>
                    </div>
                {% endfor %}
            </div>

            <div class="column is-4">
                <h2>Categories</h2>

                {% for category in categories %}
                    <div>
                        <p>
                            <a href="{% url 'category_detail' category.id %}">{{ category.title }}</a>
                        </p>
                    </div>
                {% endfor %}
            </div>
        </div>
    </div>
{% endblock %}

There isn't too many changes, we just added a link to the category detail page around the category title.

We use a template function called "url" which comes from django. Here we use the name of the path (which we defined in urls.py). And we pass in the category id. Based on this, Django will return the correct url to the category page.

You can now go to the browser and test this out :-)

Right now, all of the urls we have in our Django project is placed inside the urls.py file in our main folder. This isn't the best solution, because the file can be very long and unmaintainable. It also doesn't make much sense to import all of the view to this file.

So what we want to do now is to create separate urls.py file for the apps. One for the task app and one for the category app.

Create a new file called urls.py inside the task app. It should look like this:

from django.urls import path

from . import views

urlpatterns = [
    path('', views.frontpage, name='frontpage'),
]

As you can see, this is the same path as we used in our main urls file. Only difference is that we added a "views." infront of the view name. You can now remove this url from the main urls.py file.

Next, let's do this for the category as well. That file should look like this:

from django.urls import path

from . import views

urlpatterns = [
    path('<int:pk>/', views.category_detail, name='category_detail'),
]

So you can now remove this from the main file as well.

The problem now is that Django doesn't know that these urls exists any longer. So if you visit the site now, you will get a 404 error.

To fix this, we can import these two files to the main urls.py file. It should look like this:

from django.urls import path, include # Change, 1

urlpatterns = [
    path('', include('task.urls')),
    path('', include('category.urls')),
]

What happens now is that if you go to the front page, it will first go into the urls in the task app. And yes, it will find the front page and render it.

If you go to "/1/", it will first go into the urls file in the task app. But there isn't any paths that will match. So it will continue to the category app, and there it will find the category detail page.

Maybe the main urls.py doesn't look much cleaner right now, but as soon as we start adding more pages to the different apps, you will understand why we do this.

Django forms

When we work with input from users in Django, we almost always want to use forms. I'm not just talking about HTML forms now, but a feature from Django.

By doing this, we keep best pratices for security and similar. The first thing we can do is to create a new file called "forms.py" in the "task" app folder. We could actually call this whatever we want, but forms.py makes sense and it's best practice to call it that :-)

The file should look like this:

from django.forms import ModelForm

from .models import Task

class TaskForm(ModelForm):
    class Meta:
        model = Task
        fields = ('title', 'description', 'is_done', 'category')

This little snippet of code gives us a lot of functionality. Like valitation, it helps us store the info correctly, it helps us show the form and similar.

In this case, we use something called a "ModelForm". A ModelForm takes a model (Taks) and a list of fields. Next step now is to do some changes to the front page view:

from django.shortcuts import render

from .forms import TaskForm # New, 1
from .models import Task
from category.models import Category

def frontpage(request):
    form = TaskForm() # New, 2
    title = 'This is a variable'
    tasks = Task.objects.all()
    categories = Category.objects.all()

    return render(request, 'task/frontpage.html', {'title': title, 'tasks': tasks, 'categories': categories, 'form': form}) # Changed, 3

1. First, we import the form we just created.
2. Then, we create a new instance of it.
3. Then to make it available in the templat, we add it to the context list.

Next step then is to show it in the template. Open up frontpage.html and make it look like this:

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

{% block content %}
    <div class="frontpage">
        <h1>{{ title }}</h1>

        <div class="columns">
            <div class="column is-8">
                <h2>Tasks</h2>

                <!-- New -->
                <form method="post" action=".">
                    {% csrf_token %}

                    {{ form.as_p }}

                    <button>Submit</button>
                </form>
                <!-- End -->

                {% for task in tasks %}
                    <div>
                        <p>{{ task.title }}</p>
                    </div>
                {% endfor %}
            </div>

            <div class="column is-4">
                <h2>Categories</h2>

                {% for category in categories %}
                    <div>
                        <p>
                            <a href="{% url 'category_detail' category.id %}">{{ category.title }}</a>
                        </p>
                    </div>
                {% endfor %}
            </div>
        </div>
    </div>
{% endblock %}

When we use "post" as the request method for a form, we always need to add something called a csrf_token. This is a security step from Django. This is a way to make it impossible/harder to make the request from other sites.

Then, to print the form, we just say {{ form.as_p }}.
This will render the form fields as paragraphs. You could also try {{ form.as_table }} if you wanted to.

It doesn't look very pretty, but it could be customized if you wanted to :-)

Submitting the form

If you try the form now, nothing would happen. Because we don't do anything with it in the view, we just created an empty instance of it. We need to check if the form has been submitted, if it's valid and similar.

from django.shortcuts import render

from .forms import TaskForm
from .models import Task
from category.models import Category

def frontpage(request):
    if request.method == 'POST':
        form = TaskForm(request.POST)

        if form.is_valid():
            form.save()
    else:
        form = TaskForm()

    title = 'This is a variable'
    tasks = Task.objects.all()
    categories = Category.objects.all()

    return render(request, 'task/frontpage.html', {'title': title, 'tasks': tasks, 'categories': categories, 'form': form})

Now we added a bunch of new lines here. But it's almost a bit self explainable. First, we check if the method of the request is POST. If so, we know that the form has been submitted.

And if if has been submitted, we want to create an instance of the form where we pass in the POST data. When that's done, we check if it's valid and save it. If there are errors, Django will show the errors in the template automatically.

If it's not a POST request, we just create an empty instance of the form to show it.

Summary

We have now successfully made it possible to add data to our project. Next step now is to make it possible to edit tasks. So tomorrow, we will keep learning about Django forms.

We are actually just going to continue to use the same form as earlier, but I want a separate view and template for editing the tasks.

Let's begin with the template. Create a new file (called edit_task.html) in the same folder as the frontpage.html. It should look like this:

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

{% block content %}
    <div class="edit-tasks">
        <h1>Edit task</h1>

        <form method="post" action=".">
            {% csrf_token %}

            {{ form.as_p }}

            <button>Submit</button>
        </form>
    </div>
{% endblock %}

Hopefully, all of the code here looks familiar, and that you understand it?
We don't need to do anything more here in the template. Django will fill out the form and similar, based on the data we provide it from the view.

If we open up task/views.py again, we can create the view for editing tasks:

from django.shortcuts import render, redirect # 1, New

...

def edit_task(request, pk):
    task = Task.objects.get(pk=pk)

    if request.method == 'POST':
        form = TaskForm(request.POST, instance=task)

        if form.is_valid():
            form.save()

            return redirect('frontpage')
    else:
        form = TaskForm(instance=task)
    
    return render(request, 'task/edit_task.html', {'form': form})

First, we import a new shortcut from Django called "redirect". This will be used for redirecting the user back to the front page after the task is saved. I added three dots "..." just to show you that the other imports and the frontpage view can be like they were.

The code inside this view is more or less the same as the frontpage. We check the request method, and create a form based on this. The main difference is the instance parameter that we pass in. This is to make sure that Django know that we are editing a task. So since we passed in this parameter, Django will automatically fill the form for us and also make sure the task is updated and not created.

Before we can use this, we also need to append the view to "task/urls.py" like this:

from django.urls import path

from . import views

urlpatterns = [
    path('', views.frontpage, name='frontpage'),
    path('edit_task/<int:pk>/', views.edit_task, name='edit_task'), # New
]

This should look familiar. It's just a simple primary key inside the path, and then we point to the view. Next and last step is to update template to link to this path. In the loop where we show the tasks, do this:

{% for task in tasks %}
    <div>
        <p>
            {{ task.title }}
            -
            <a href="{% url 'edit_task' task.id %}">Edit</a>
        </p>
    </div>
{% endfor %}

And that was it, this was everything we needed to do to make it possible to edit tasks.

Mark as done / completed

The first thing I want to do for making it possible to complete a task, is to create a new view. Open up "task/views.py" and add this new view:

def mark_completed(request, pk):
    task = Task.objects.get(pk=pk)
    task.is_done = True
    task.save()

    return redirect('frontpage')

So that was really easy, huh?
We just get the task from the database based on the id/pk in the url, then we set the "is_done" field to "True" and redirect the user back to th front page.

Before we can use this, we also need to append the view to "task/urls.py" like this:

from django.urls import path

from . import views

urlpatterns = [
    path('', views.frontpage, name='frontpage'),
    path('edit_task/<int:pk>/', views.edit_task, name='edit_task'),
    path('mark_completed/<int:pk>/', views.mark_completed, name='mark_completed'), # New
]

This should look familiar. It's just a simple primary key inside the path, and then we point to the view. Next and last step is to update template to link to this path. In the loop where we show the tasks, do this:

{% for task in tasks %}
    <div>
        <p>
            {{ task.title }}
            -
            <a href="{% url 'edit_task' task.id %}">Edit</a>
            -
            {% if task.is_done %}
                Completed
            {% else %}
                <a href="{% url 'mark_completed' task.id %}">Mark complete</a>
            {% endif %}
        </p>
    </div>
{% endfor %}

So here is a little bit of new code. We're using an if-statement to check if the is_done field is set to true or not. If the value is true, we just show a label saying "Completed". But it the value is false, then we show a link which will be used for marking the task as done.

Deleting tasks

The procedure for deleting the tasks is almost the same. Let's begin with the view.

def delete_task(request, pk):
    task = Task.objects.get(pk=pk)
    task.delete()

    return redirect('frontpage')

So here we do almost the same thing as in the other view, the only difference is that we call a function called "delete()" on the task object. And Django will handle the rest for us. Let's add it to the url patterns:

from django.urls import path

from . import views

urlpatterns = [
    path('', views.frontpage, name='frontpage'),
    path('edit_task/<int:pk>/', views.edit_task, name='edit_task'),
    path('mark_completed/<int:pk>/', views.mark_completed, name='mark_completed'),
    path('delete_task/<int:pk>/', views.delete_task, name='delete_task'), # New
]

Last step then is to add a link for this in the loop at the front page again:

{% for task in tasks %}
    <div>
        <p>
            {{ task.title }}
            -
            <a href="{% url 'edit_task' task.id %}">Edit</a>
            -
            {% if task.is_done %}
                Completed
            {% else %}
                <a href="{% url 'mark_completed' task.id %}">Mark complete</a>
            {% endif %}
            -
            <a href="{% url 'delete_task' task.id %}">Delet</a>
        </p>
    </div>
{% endfor %}

Go ahead and test it out. We should now be able to mark tasks as completed and we can also delete them if we wanted to.

All of our tasks can have titles and descriptions. Let's say that you have set up over 100 tasks. It wouldn't be very effective to go through the whole list when you're looking for a specific task. So let's make it possible to search.

The first thing we want is a new view. Let's put it in the task/views.py file.

from django.db.models import Q # New, 1
...

def search(request):
    query = request.GET.get('query', '')
    
    if query:
        tasks = Task.objects.filter(Q(title__icontains=query) | Q(description__icontains=query))
    else:
        tasks = []
    
    return render(request, 'task/search.html', {'query': query, 'tasks': tasks})

First, I imported a new function from Django. This let's ut build a little bit more advanced QuerySets.

Then I create the new search view. We get the "query" or search term from the url. If it exists, we get the tasks from the database by using the new Q function. Here we check if the title field contains the word we are searching for or if the description contains it. "icontains" means that it ignores capital/lowercase letters. If you used just "contains", you would have to write the letters 100% correctly.

Let's add this view to the urls.py file:

from django.urls import path

from . import views

urlpatterns = [
    path('', views.frontpage, name='frontpage'),
    path('edit_task/<int:pk>/', views.edit_task, name='edit_task'),
    path('mark_completed/<int:pk>/', views.mark_completed, name='mark_completed'),
    path('delete_task/<int:pk>/', views.delete_task, name='delete_task'),
    path('search/', views.search, name='search'), # New
]

If you tested this in your browser now, you would get a "template does not exist error". So let's create it now.

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

{% block content %}
    <div class="frontpage">
        <h1>Search</h1>

        <h2>You searched for "{{ query }}"</h2>

        {% for task in tasks %}
            <div>
                <p>{{ task.title }}</p>
            </div>
        {% empty %}
            <p>No tasks matched your query...</p>
        {% endfor %}
    </div>
{% endblock %}

As you can see, a lot of this is copied from the front page template. I added one new tag here "{% empty %}". This checks if the list is empty or not. So if you search for something that doesn't exists, you will see the "No tasks matched your query..." message instead.

Great, now the search functionality is implemented and is hopefully working. Let's add a simple search bar to the top of base.html.

<doctype html!>
<html>
    <head>
        <title>Toodoo</title>
    </head>

    <body>
        <nav>
            The menu
        </nav>

        <div>
            <form method="get" action="{% url 'search' %}">
                <input type="search" name="query" placeholder="Find a task...">
                <button>Search</button>
            </form>
        </div>

        {% block content %}
        {% endblock %}

        <footer>
            The footer
        </footer>
    </body>
</html>

When we use forms with the method attribute set to "get" instead of "post", we do not need a csrf token.

Nice! You can now test that everything is working as expected :-)

I'm going to use a CSS framework called Bulma for this project. The main goal of this project is to learn Django, so I'm not going to go into much details for this code.

Let's begin with the base.html file:

<doctype html!>
<html>
    <head>
        <title>Toodoo</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
    </head>

    <body>
        <nav class="navbar is-dark">
            <div class="navbar-brand">
                <a href="/" class="navbar-item is-size-4">Toodoo</a>
            </div>
        </nav>

        <section class="section">
            <div class="columns">
                <div class="column is-6 is-offset-3">
                    <form method="get" action="{% url 'search' %}">
                        <div class="field has-addons">
                            <div class="control">
                                <input type="search" class="input" name="query" placeholder="Find a task...">
                            </div>

                            <div class="control">
                                <button class="button is-primary">Search</button>
                            </div>
                        </div>
                    </form>

                    <hr>

                    {% block content %}
                    {% endblock %}
                </div>
            </div>
        </section>

        <footer class="footer">
            The footer
        </footer>
    </body>
</html>

Okay. This already looks much better.
Next step, the frontpage.

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

{% block content %}
    <div class="frontpage">
        <div class="columns">
            <div class="column is-8">
                <h2 class="subtitle">Tasks</h2>

                <form method="post" action=".">
                    {% csrf_token %}

                    {{ form.as_p }}

                    <div class="field">
                        <button class="button is-primary">Submit</button>
                    </div>
                </form>

                {% for task in tasks %}
                    <div class="mb-4 px-4 py-4 has-background-grey-lighter">
                        <p>
                            {{ task.title }}
                            -
                            <a href="{% url 'edit_task' task.id %}">Edit</a>
                            -
                            {% if task.is_done %}
                                Completed
                            {% else %}
                                <a href="{% url 'mark_completed' task.id %}">Mark complete</a>
                            {% endif %}
                            -
                            <a href="{% url 'delete_task' task.id %}">Delete</a>
                        </p>
                    </div>
                {% endfor %}
            </div>

            <div class="column is-4">
                <h2 class="subtitle">Categories</h2>

                {% for category in categories %}
                    <div>
                        <p>
                            <a href="{% url 'category_detail' category.id %}">{{ category.title }}</a>
                        </p>
                    </div>
                {% endfor %}
            </div>
        </div>
    </div>
{% endblock %}

Okay, it doesn't look very good, but not very bad either. The other pages can just be like they are. Of course, feel free to change them as wll if you want to.

Making it possible for users to sign up is very easy thanks to the built in authentication system in Django. Let's begin with creating a new Django app for users:

$ python manage.py startapp userprofile

And then we need to append it to a list in the "settings.py" file.

INSTALLED_APPS = [
    ...
    'userprofile',
]

Django comes with a built in database model for users, so this app will only be used for views and templates. Open up userprofile/views.py, it should look like this:

from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm
from django.shortcuts import render, redirect

def signup(request):
    if request.method == 'POST':
        form = UserCreationForm(request.POST)

        if form.is_valid():
            user = form.save()

            login(request, user)

            return redirect('frontpage')
    else:
        form = UserCreationForm()
    
    return render(request, 'userprofile/signup.html', {'form': form})

Most of this code should hopefully start looking familiar and make sense now?
We import a method from Django for logging in the user, and a form for creating the user. If the form is valid, we store the form in a variable called "user", then we log in the user and redirect to the front page.

Next step then is to create the folder for the template (templates/userprofile) and the template inside "signup.html". It should look like this:

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

{% block content %}
    <div class="edit-tasks">
        <h1>Sign up</h1>

        <form method="post" action=".">
            {% csrf_token %}

            {{ form.as_p }}

            <button>Sign up</button>
        </form>
    </div>
{% endblock %}

So yet again, a very simple template where we render a form.

The last step then is to create a url file and also appending it in the main urls.py file. Create a file called urls.py in the new userprofile app:

from django.urls import path

from . import views

urlpatterns = [
    path('sign-up/', views.signup, name='signup'),
]

And then add it in the main urls.py file:

from django.urls import path, include

urlpatterns = [
    path('', include('userprofile.urls')), # New
    path('', include('task.urls')),
    path('', include('category.urls')),
]

Try to create a user and see what happens. In the next part, we will handle log in and making a few checks if the user is authenticated or not.

First, let's begin with making it possible to log in. Django has a built in view for this, so the only thing we need to handle is the url and the templates.

Open up userprofile/urls.py and make the following changes:

from django.contrib.auth import views as auth_views # 1. New
from django.urls import path

from . import views

urlpatterns = [
    path('sign-up/', views.signup, name='signup'),
    path('log-in/', auth_views.LoginView.as_view(template_name='userprofile/login.html'), name='login'), # 2. Change
]

1. First, we import all of the authentication views from Django. And we import them "as auth_views", so the names doesn't crash with ours.
2. Then, we append the path to the urlpatterns. This is a Class Based View, and the only thing we need to specify is where the template is located. Django will handle all of the magic for us.

Create a new file called "login.html", and copy the contents from signup.html.

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

{% block content %}
    <div class="edit-tasks">
        <h1>Log in</h1>

        <form method="post" action=".">
            {% csrf_token %}

            {{ form.as_p }}

            <button>Log in</button>
        </form>
    </div>
{% endblock %}

You can just rename the title and button.

We didn't see in the url file that there was supposed to be a variable called "form", but this is the default way for Django to handle this. So now you can try this out by going to "/log-in/" in your browser.

Last step of this part is to make some changes in the base.html file:

...

<nav class="navbar is-dark">
    <div class="navbar-brand">
        <a href="/" class="navbar-item is-size-4">Toodoo</a>
    </div>

    <div class="navbar-menu">
        <div class="navbar-end">
            {% if request.user.is_authenticated %}
                <a class="navbar-item">Log out</a>
            {% else %}
                <a href="{% url 'signup' %}" class="navbar-item">Sign up</a>
                <a href="{% url 'login' %}class="navbar-item">Log in</a>
            {% endif %}
        </div>
    </div>
</nav>

...

So here we add a new menu inside the navigation bar. We use the if-tag from Django to check if the user is authenticated. If the user is not authenticated, we show the sign up and login links. But if the user is authenticated, we show a log out link (Will be implemented in the next part).

Django logout function

Let's begin by editing the userprofile/urls.py file. We need to add the path to the logout page here:

from django.contrib.auth import views as auth_views
from django.urls import path

from . import views

urlpatterns = [
    path('sign-up/', views.signup, name='signup'),
    path('log-in/', auth_views.LoginView.as_view(template_name='userprofile/login.html'), name='login'),
    path('log-out/', auth_views.LogoutView.as_view(), name='logout'), # New
]

And there you have it, we can now log out by using Django's built in function for this. Let's link to this in the base.html file before we continue.

...

<nav class="navbar is-dark">
    <div class="navbar-brand">
        <a href="/" class="navbar-item is-size-4">Toodoo</a>
    </div>

    <div class="navbar-menu">
        <div class="navbar-end">
            {% if request.user.is_authenticated %}
                <a href="{% url 'logout' %}" class="navbar-item">Log out</a> 
            {% else %}
                <a href="{% url 'signup' %}" class="navbar-item">Sign up</a>
                <a href="{% url 'login' %}class="navbar-item">Log in</a>
            {% endif %}
        </div>
    </div>
</nav>

...

If you log out now, you will be redirected to a page that doesn't exist or that looks like a part of the admin interface. Let's reconfigure this, so we can control where the user is redirected to.

Open up settings.py, and add the three lines somewhere (It does not matter where):

LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = 'frontpage'
LOGOUT_REDIRECT_URL = 'login'

LOGIN_URL: This is the page you are redirected to if you go to a page that you're not authorized to see.
LOGIN_REDIRECT_URL: This is the page you are redirected to after you log in.
LOGOUT_REDIRECT_URL: This is the page you are redirected to after you log out.

Guarding views / paths

Right now if you go to the front page, you will get an error because you're not logged in. We need to change this a little bit. We don't want an error.

We want to tell Django that you need to be authenticated to view this page, and then Django will automatically redirect you if you're not logged in.

from django.contrib.auth.decorators import login_required # 1. New
from django.shortcuts import render

...

@login_required # 2. New
def frontpage(request):
    ...

1. First we import something called a decorator. A decorator adds "configurations"/"rules" to a view.
2. Then we add the decorator above the view.

Add this decorator above all of the other views as well. Also in the category app. Just don't add them in the signup view :-)

Updating the task model

We made it possible to log in to the project, but there is a problem. Everyone sees the same data. So it's time to fix that. Open up task/models.py:

from django.contrib.auth.models import User # New, 1
...

Class Task(models.Model):
    user = models.ForeignKey(User, related_name='tasks', on_delete=models.CASCADE) # New, 2
    category = models.ForeignKey(Category, related_name='tasks', on_delete=models.CASCADE)
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True, null=True)
    is_done = models.BooleanField(default=False)

1. First, we import the user model (The same we use for registering).
2. The, we add a new field to the model.

Next step then is to update the database again:

$ python3 manage.py makemigrations

And then run the migrations scripts:

$ python3 manage.py migrate

When this is done, the task table in the database should be updated. NB NB NB:
You might be asked to set a default user or similar here, just type 1 on your keyboard.

Get only your tasks

Next step is to update task/views.py:

from django.contrib.auth.models import User # New, 1
...

@login_required
def frontpage(request):
    if request.method == 'POST':
        form = TaskForm(request.POST)

        if form.is_valid():
            form.save()
    else:
        form = TaskForm()

    title = 'This is a variable'
    tasks = Task.objects.filter(user=request.user) # Change, 2
    categories = Category.objects.all()

    return render(request, 'task/frontpage.html', {'title': title, 'tasks': tasks, 'categories': categories, 'form': form})

@login_required
def search(request):
    query = request.GET.get('query', '')
    
    if query:
        tasks = Task.objects.filter(user=request.user).filter(Q(title__icontains=query) | Q(description__icontains=query)) # Change, 3
    else:
        tasks = []
    
    return render(request, 'task/search.html', {'query': query, 'tasks': tasks})

@login_required
def edit_task(request, pk):
    task = Task.objects.filter(user=request.user).get(pk=pk) # Change, 4

    if request.method == 'POST':
        form = TaskForm(request.POST, instance=task)

        if form.is_valid():
            form.save()

            return redirect('frontpage')
    else:
        form = TaskForm(instance=task)
    
    return render(request, 'task/edit_task.html', {'form': form})

@login_required
def mark_completed(request, pk):
    task = Task.objects.filter(user=request.user).get(pk=pk) # Change, 5
    task.is_done = True
    task.save()

    return redirect('frontpage')

@login_required
def delete_task(request, pk):
    task = Task.objects.filter(user=request.user).get(pk=pk) # Change, 6
    task.delete()

    return redirect('frontpage')

1. First, we import the user database model again.
2, 3, 4, 5, 6. Here, we add a new filter. To make sure that the user field matches the authenticated user.

Great, all of the get commands should now work. You should only see data connected to your user. Last step then is to make sure that when you create a new task, it's added to your user.

@login_required
def frontpage(request):
    if request.method == 'POST':
        form = TaskForm(request.POST)

        if form.is_valid():
            task = form.save(commit=False) # New
            task.user = request.user # New
            task.save() # New
    else:
        form = TaskForm()

    title = 'This is a variable'
    tasks = Task.objects.filter(user=request.user)
    categories = Category.objects.all()

    return render(request, 'task/frontpage.html', {'title': title, 'tasks': tasks, 'categories': categories, 'form': form})

We added three new lines here. Since we added a new field called user, we can't just run form.save(). Because this will lead to an error, since the user field isn't filled out. So we create a new variable called task, and assign all of the data to it. Next, we set the user field to the authenticated user and save()

We generally want to have as simple views as possible, because it doesn't make sense to put functionality there. Functionality should be put in model files or utility files.

So in this part of the series, we will add a custom function to one of our models. Open up "category/models.py":

class Category(models.Model):
    title = models.CharField(max_length=255)
    
    class Meta:
        verbose_name_plural = 'Categories'
        
    def __str__(self):
        return self.title
        
    def number_of_tasks(self):
        return self.tasks.count()

As you can see here, we've added a new method to the Category model. This could now be used in templates and similar like this:

{{ category.number_of_tasks }}

A template filter is used to work with and manipulate data in our templates. They can be used for a lot of things like date formatting, humanizing, transform to uppercase and similar.

In this part, I will begin by showing th date filter and after that, the naturaltime filter. But first, we need to add a date field to our tasks.

...

Class Task(models.Model):
    user = models.ForeignKey(User, related_name='tasks', on_delete=models.CASCADE)
    category = models.ForeignKey(Category, related_name='tasks', on_delete=models.CASCADE)
    title = models.CharField(max_length=255)
    description = models.TextField(blank=True, null=True)
    is_done = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True) # 1, new

1. Here we added the "created_at" and set it to be a DateTimeField. This has both date and time. We use a parameter called "auto_now_add", this makes sure that when we add a task. It will be filled in automatically.

Next step then is to update the database again:

$ python3 manage.py makemigrations

And then run the migrations scripts:

$ python3 manage.py migrate

Great, next step then is the template. Open up frontpage.html:

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

{% block content %}
    <div class="frontpage">
        <h1>{{ title }}</h1>

        <div class="columns">
            <div class="column is-8">
                <h2>Tasks</h2>

                ..

                {% for task in tasks %}
                    <div>
                        <p>{{ task.title }} - {{ task.created_at }}</p>
                    </div>
                {% endfor %}
            </div>

            ...
        </div>
    </div>
{% endblock %}

If you run this now, you will see a timestamp next to the title. Let's change it a little bit:

{{ task.created_at|date:"Y-m-d" }}

If you run this now, you will see the year-month-day. Let's add hours and minutes as well:

{{ task.created_at|date:"Y-m-d H:i" }}

So this is how you can add filters with parameters. Let's try a different one:

{{ task.created_at|naturaltime }}

If you run this now, you will see how long time it has been since the task was created.

Django has built in support and functionality for paginating data. So this will be utilized in this Django project.

Let's open up the task/views.py file, and begin there with these changes:

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger # New, 1
...

@login_required
def frontpage(request):
    if request.method == 'POST':
        form = TaskForm(request.POST)

        if form.is_valid():
            form.save()
    else:
        form = TaskForm()

    title = 'This is a variable'
    tasks = Task.objects.filter(user=request.user)
    categories = Category.objects.all()

    page = request.GET.get('page', 1)
    paginator = Paginator(tasks, 10)
    try:
        tasks = paginator.page(page)
    except PageNotAnInteger:
        tasks = paginator.page(1)
    except EmptyPage:
        tasks = paginator.page(tasks.num_pages)

    return render(request, 'task/frontpage.html', {'title': title, 'tasks': tasks, 'categories': categories, 'form': form})

As you can see here, there are a few new lines. First, we import three methods from the paginator class.

Then, we just get all of the tasks like we've done earlier.
Next step is to get the page number from the URL, if it's not there, we default it to 1.

Next, we set up a Paginator, pass in the tasks and the limit per page (10). Then we check if there is an actual page, if the page is empty and simlar.

So when that's done, we can go to the template and implement it there. Open up frontpage.html:

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

{% block content %}
    <div class="frontpage">
        <h1>{{ title }}</h1>

        <div class="columns">
            <div class="column is-8">
                <h2>Tasks</h2>

                ..

                {% for task in tasks %}
                    <div>
                        <p>{{ task.title }} - {{ task.created_at }}</p>
                    </div>
                {% endfor %}

                {% if tasks.has_other_pages %}
                    <nav class="pagination" role="navigation" aria-label="pagination">
                        {% if posts.has_previous %}
                            <a class="pagination-previous" href="?page={{ tasks.previous_page_number }}">Previous</a>
                        {% endif %}

                        {% if tasks.has_next %}
                            <a class="pagination-next" href="?page={{ tasks.next_page_number }}">Next</a>
                        {% endif %}

                        <ul class="pagination-list">
                            {% for i in tasks.paginator.page_range %}
                                {% if tasks.number == i %}
                                    <li>
                                        <a class="pagination-link is-current">{{ i }}</a>
                                    </li>
                                {% else %}
                                    <li>
                                        <a class="pagination-link" href="?page={{ i }}">{{ i }}</a>
                                    </li>
                                {% endif %}
                            {% endfor %}
                        </ul>
                    </nav>
                {% endif %}
            </div>

            ...
        </div>
    </div>
{% endblock %}

The code for this is more or less self explanatory. We use the tasks object to check if there are pages, if there are any previous pages and similar.

Let's say that we want to show a message to the user when he or she adds a task. How do we do that? Well, the most simple way would be to use Django's built in messages framework.

If you open up settings.py, you'll notice that it's already added to the list of INSTALLED_APPS.

So let's open up the task/views.py and do some changes:

from django.contrib import messages # New, 1
...

@login_required
def frontpage(request):
    if request.method == 'POST':
        form = TaskForm(request.POST)

        if form.is_valid():
            form.save()

            messages.success(request, 'The task was added', extra_tags='is-success') # New, 2
    else:
        form = TaskForm()

    title = 'This is a variable'
    tasks = Task.objects.filter(user=request.user)
    categories = Category.objects.all()

    page = request.GET.get('page', 1)
    paginator = Paginator(tasks, 10)
    try:
        tasks = paginator.page(page)
    except PageNotAnInteger:
        tasks = paginator.page(1)
    except EmptyPage:
        tasks = paginator.page(tasks.num_pages)

    return render(request, 'task/frontpage.html', {'title': title, 'tasks': tasks, 'categories': categories, 'form': form})

So we didn't add more than 2 lines, but it's everything we need in the backend.
1: First, we import the method from Django.
2: Then, we add a new message to the session after the task is gone.

We specify the text and also add a optional parameter called "extra_tags". This is not necessary, but I want to do it to get some styling from Bulma.

Django has a lot of other options for the messages. If you want to learn more about them, you can read it here: https://docs.djangoproject.com/en/4.0/ref/contrib/messages/

Great, let's open up the base.html file and show the messages there:

...

<nav class="navbar is-dark">
    ...
</nav>

{% if messages %}
    {% for message in messages %}
        <div class="notification {{ message.tags }}">{{ message }}</div>
    {% endfor %}
{% endif %}

...

So here we check if there are any messages, and if there are, we loop through them. The {{ message.tags }} is the "extra_tags".

So when the message is added to the system (in views.py), it's waiting to be shown. And as soon as it's shown, it will not appear again. So if you refresh, the message is gone :-)

Where to continue?

Hopefully, you've learned quite a lot of Django the last 30 days? Eventhough you've now learned the very basics of Django, there's still a long way to go to master Django.

I run a YouTube channel (Code With Stein - Django tutorials) where you can learn a ton of Django and much more.

I think that after this introduction to Django, you should be able to follow along most of my videos and series there. And that's also my suggestion for you. Go through as many tutorials as you can and expand your knowledge.

Thank you so much for following along. Feel free to leave me a comment below :-)

Stein Ove Helset

Your instructor
Hey! My name is Stein Ove Helset. I'm a self-taught software developer with over a decade of experience working full time as a web developer. Through this website and YouTube channel, I have taught thousands of people how to code, build websites, games and similar.

If you're looking for an introduction to coding and web development, you've come to the right place.