To Do List - Part 2: Apps & Models

Welcome to Part 2 of my To-Do list tutorial series.

In Part 1, we set up a Django project with a database and created a user. In this tutorial, we going to create a model, which will be used to create a table in our database.

This is what we are going to cover:

  1. Structuring your Django project with apps

  2. Registering the app

  3. Creating a model

  4. Creating and running migrations

  5. How to make the model appear in the admin panel

  6. Testing the model

Code

To follow this tutorial, you can either follow Part 1 of this tutorial series, or clone this branch of the repository.

If you get stuck, you can refer to the finished code here.

Step 1: Create an app

Django encourages developers to organise their code into “apps”. Some apps are built into Django (see INSTALLED_APPS in settings.py); some you will create yourself and some will be created by a 3rd party (e.g. a text editor).

The app architecture encourages you to modularise your code. If you would like to understand it more, I have written a separate article on what they are and they they are useful.

For now, we just need to create an app, which will contain our model, view and templates:

python manage.py startapp todo

This will create a folder called todo.

If you can’t call your app todo because it’s your project name, a common alternative is to use core.

A diagram showing the folder structure. The to-do app is a directory containing the following files: init, admin, apps, models, tests and views. It also contains a folder called migrations.

What have we created?

  • A directory called migrations. This will store migration files which when run, will make changes to the structure and specification of the database. If you would like to understand more about migrations, I have a beginner’s guide.

  • __init__.py is an empty file that indicates to Python that the folder is a Python module.

  • admin.py stores configuration to display our models in the admin area. I also have a beginner’s guide to admin.

  • apps.py contains the AppConfig for the app. It is rare that you will need to make changes to this file.

  • models.py is where we will define the schema for tables in our database. Understanding models is essential to understanding Django. You can learn more about models with my beginner’s guide.

  • tests.py will contain the tests for this app.

  • views.py will contain the logic that maps HTTP requests into responses. If you are familiar with the MVC architecture, you should know that views.py is the equivalent of a Controller and a Django Template is the equivalent of an MVC view.

Step 2: Register the app (important!)

You will need to add the app name to the list of INSTALLED_APPS in settings.py.

If you forget this step, you won’t be able to create any migrations from your models.

My INSTALLED_APPS now looks like this:

# settings.py

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "todo",
]

Step 3: Create a model

To create a table in the database, we need to create a class in models.py. This class should inherit from django.models.Model which will give our application the ability to detect changes in the model (to create migrations) and allow us to use it to create objects in the database.

This is what my models.py file looks like. We’ll go through it step by step:

# todo/models.py

from django.db import models


class Task(models.Model):
    class StatusChoice(models.TextChoices):
        TODO = "To Do"
        DOING = "Doing"
        DONE = "Done"

    name = models.CharField(max_length=255)
    created = models.DateTimeField(auto_now_add=True)
    status = models.CharField(
        choices=StatusChoice.choices, max_length=100, default=StatusChoice.TODO
    )

    def __str__(self):
        """Provides a readable reference for each object"""
        return f"{self.status}: {self.name}"

The Task table will store the items of our To Do List. It will have four columns: ID, name, created and status. The ID (also known as Primary Key or PK) is created for us. We don’t need to define it in the model.

The name field is a CharField meaning the column will store a string. It is required to specify the max_length attribute. 255 is the maximum length for a CharField.

The created field will store the date and time that the item was created. By adding auto_add_now=True, the value is automatically populated with the current date/time immediately before the object is saved to the database.

The status field indicates if the task is pending, in progress, or complete. I have used a CharField because I want to use a string to store the status. However, I don’t want any string to be used. I only want the status to have values of “To Do”, “Doing” or “Done.

The choices attribute stores a tuple of allowed values. I have stored the choices in a subclass called StatusChoice. The StatusChoice class inherits from models.TextChoice. In forms, CharFields with choices are rendered with a drop-down menu. However, for this project I will set the default to StatusChoice.TODO.

Finally, I have added a method to the class called __str__. This provides a readable reference to each task object. You will see this being used when we add the model to our admin area.

Step 4: Create and run migrations

Our Task class in models.py specifies what our task table will look like but the table doesn’t exist in the database yet.

Django provides a command called makemigrations. On running this command, Django will look for changes to models and create files that summarise the changes that will be made to the database.

python manage.py makemigrations

You should get an output like this:

python manage.py makemigrations

Migrations for 'todo':
  todo/migrations/0001_initial.py
    - Create model Task

Look inside your migrations folder. You will now have a file called 0001_initial.py.

Creating migrations does not affect the database, we have to run them. When a migration is run, Django will convert the contents of the migration file into SQL and execute the SQL code on the database.

python manage.py migrate

Note that migrations are only safe to delete before they have been run. Never delete migration files after running them. Instructions on how to reverse migrations can be found here.

Step 5: Register the model in admin.py

Right now, our new model doesn’t appear in the admin area (localhost:8000/admin).

To make it appear, we need to register the model in admin.py.

In todo/admin.py, add the following code:

from django.contrib import admin
from todo import models

admin.site.register(models.Task)

Save the file and refresh admin.

You should now see a section called “Todo” with an item of “Tasks”.

Click on “Tasks” and you will be able to create tasks for your To Do list.

Screenshot of the admin panel demonstrating that you can create tasks for your todo list in admin.

Step 6: Add tests (optional)

Our Task model has been designed so that users only need to supply the task name. The status field will receive a value of “To Do” by default and the created field will get the current date and time.

We can write some tests to verify this.

Go to tests.py and add the following code, or refer to tests.py on GitHub.

import datetime

from django.test import TestCase

from todo.models import Task


class TestTaskModel(TestCase):
    @classmethod
    def setUpTestData(cls):
        """Creates a task instance that our tests can access.
        setUpTestData is executed once, so our task instance is
        shared between tests.
        """
        cls.task_name = "Book dentist appointment"
        cls.task = Task.objects.create(name=cls.task_name)

    def test_task_name(self):
        self.assertEqual(self.task.name, self.task_name)

    def test_task_default_status(self):
        """Test that our task was assigned a status of 'To Do'"""

        expected = Task.StatusChoice.TODO
        actual = self.task.status

        self.assertEqual(expected, actual)

    def test_task_created(self):
        """Tests that task.created stores a datetime"""

        self.assertEqual(type(self.task.created), datetime.datetime)

    def test_str(self):
        expected = f"To Do: {self.task_name}"
        actual = str(self.task)

        self.assertEqual(expected, actual)

I have used setUpTestData to create a task object to be shared by all tests. This means I don’t have to create a new task for every single test.

I have added tests to check the name, the default status and check that a datetime was saved to the created field.

I have also tested the __str__ method.

To run the tests, run the following command in your terminal:

python manage.py test

You should get the following output:

python manage.py test

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

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

To learn more about how to write tests for your Django applications, I have a beginner’s guide and a guide to testing models.

Step 7: Create fake data (optional)

It is possible to automatically create data to populate our Task table. In this tutorial, I will show you how to write a script that creates tasks in the database using Faker to generate task names.

Next steps:

In this tutorial, we created a table in the database to store tasks for our To Do list.

In the next tutorial, we will add URLS, a view and a template so that we can view our To Do list in the browser.