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:
Structuring your Django project with apps
Registering the app
Creating a model
Creating and running migrations
How to make the model appear in the admin panel
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
.
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 theAppConfig
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 thatviews.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.
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.