Django is a fantastic technology that has proven itself as a solid and reliable Python Web Development Framework. Bulma is a new and really cool CSS Framework for making your front end look as good as it can. In this tutorial, we will use Django on the back end with Bulma CSS on the front end to build a small Todo application. We’ll see how to add items to a list, style them accordingly, mark items as complete or incomplete, as well as a few other tricks. Let’s get started with a Todo application in Django and Bulma now.
Getting Started
This tutorial assumes you have Python and Django installed and running, ready to start building your application. If you need help getting those aspects ready to go, Django For Beginners might be a good first read.
Start A New Django Project
We can begin by starting a new Django project using the command django-admin.py startproject todo.
(vdjango) vdjango $ django-admin.py startproject todo
Then, we’ll quickly start up the Django server using python manage.py runserver just to confirm everything is working, and things look good!
(vdjango) todo $python manage.py runserver Watching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). You have 17 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions. Run 'python manage.py migrate' to apply them. May 18, 2020 - 09:49:58 Django version 3.0.6, using settings 'todo.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CTRL-BREAK.
With the Django project scaffolded out, we can cd into the project directory and run the built-in migrations using python manage.py migrate.
(vdjango) vdjango $cd todo (vdjango) todo $python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying sessions.0001_initial... OK (vdjango) todo $
Visiting The Admin Dashboard
Django has a great administrative dashboard that you get for free. We can check it out by visiting http://127.0.0.1:8000/admin in the web browser of your choice.
Now to make use of this admin area, we need a superuser to do so. We can easily create one with the python manage.py createsuperuser command.
(vdjango) todo $python manage.py createsuperuser Username (leave blank to use 'compname'): admin Email address: admin@example.com Password: Password (again): Superuser created successfully. (vdjango) todo $
Use the credentials you provided when creating your superuser to log in to the admin area and have a look around.
Start Your App
Django is designed so that you can create reusable applications. There are many great Django Packages available you can plug and play right into your Django project. A Django project is just a collection of one or many apps. Let’s create a new app now with the command python manage.py startapp todoapp.
(vdjango) todo $python manage.py startapp todoapp
Once that command runs, you’ll see a new directory in your Django project that holds all of the files needed for the app.
Register Your App
When you add a new app to your Django project, you need to let Django know about it. This can be done in the settings.py file like we see here. We can add the todoapp like so:
todotodosettings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'todoapp'
]
Adding a urls.py file for your app
Each app needs a urls.py file to route HTTP traffic properly in your Django project. We can add that file manually now.
To get started with creating URL mappings, import the path module from django.urls.
todotodoappurls.py
from django.urls import path
urlpatterns = [
path(),
]
Using include() with urls
In the project-level directory (not our new app!), we want to set things up so that when a user visits the root URL, our Django project will point this request to the new app we just created. What the code below does is essentially take all incoming requests, and forwards them to the urls.py file that lives in the todoapp directory.
todotodourls.py
from django.contrib import admin
from django.urls import path, include
from todoapp import views
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('todoapp.urls'))
]
Adding Templates
To display a web page to the user, Django makes use of templates. Let’s add a home page template to the application. The convention is to create a template directory, and then another directory inside that one using the name of the app.
We’ll put just an HTML snippet like so.
todotodoapptemplatestodoapphome.html
<h1>Hello from home.html</h1>
Using View functions in urls.py
In the urls.py file for the app, we want to import the views module and call a function when someone requests the home page. In this code, the home() function of the views.py file will be called, and it is a named route of home. In Django, you always want to name your routes so that you can set up dynamic links.
todotodoappurls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='home'),
]
Now we can add the code needed in the views.py file to handle the request.
todotodoappviews.py
from django.shortcuts import render
def home(request):
return render(request, 'todoapp/home.html', {})
If everything goes according to plan, you should see a simple HTML page like this when you visit the site.
Adding Bulma CSS in a Base template
Now we want to accomplish two goals. One is to make our little project look stylish, and the other to reduce code repetition. We can do this by creating a base template to extend from while including the Bulma CSS library in that base template.
todotodoapptemplatestodoappbase.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django And Bulma!</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.8.2/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
</head>
<body>
{% block content %}
{% endblock content %}
</body>
</html>
Extend From Base
We can update the home.html template to extend from the base.html file we just created.
todotodoapptemplatestodoapphome.html
{% extends 'todoapp/base.html' %}
{% block content %}
<section class="section">
<div class="container">
<h1 class="title">
Hello from home.html
</h1>
</div>
</section>
{% endblock %}
Right away, we can see the font has updated and already looks better!
Adding A Navbar
Bulma has a nice-looking navigation bar you can implement quite easily. We can put the markup for the navbar in the base.html file. That way, any template file that extends from the base will automatically have the navbar included.
todotodoapptemplatestodoappbase.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django And Bulma!</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.8.2/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
</head>
<body>
<nav class="navbar is-light" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="https://bulma.io">
<img src="https://bulma.io/images/bulma-logo.png" width="112" height="28">
</a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item">
Home
</a>
</div>
<div class="navbar-end">
<div class="navbar-item">
<form>
<div class="field has-addons">
<div class="control">
<input class="input" type="text" placeholder="Add Todo">
</div>
<div class="control">
<button type="submit" class="button is-link">Submit</button>
</div>
</div>
</form>
</div>
</div>
</div>
</nav>
{% block content %}
{% endblock content %}
</body>
</html>
There we go! The navbar is looking nice even in the beginning stages.
Dynamic Data With Django Models
More fun can be had as we get into working with Models in Django. Models hold all of the data for the application. We can open up the models.py file from our application, and add the code seen here.
todotodoappmodels.py
from django.db import models
class Todo(models.Model):
task = models.CharField(max_length=200)
completed = models.BooleanField(default=False)
def __str__(self):
return self.task
Anytime you create a new Model or edit an existing model in Django, you *must* create a new migration. By running python manage.py makemigrations, Django will scan those models.py files, and build migration files automatically for you. These migration files are used to modify the database to hold data meeting the requirements of the application.
(vdjango) todo $python manage.py makemigrations Migrations for 'todoapp': todoappmigrations001_initial.py - Create model Todo (vdjango) todo $
For example, the migration below is what was created in this case.
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Todo',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('task', models.CharField(max_length=200)),
('completed', models.BooleanField(default=False)),
],
),
]
Migrate The Database
In order for those migration files to work, you need to migrate the database. That can be done by running python manage.py migrate.
(vdjango) todo $python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions, todoapp Running migrations: Applying todoapp.0001_initial... OK (vdjango) todo $
So the takeaway is that when working with Models you always:
- 1. Create The Model (or edit)
- 2. Create The Migrations
- 3. Run The Migrations
Add Your Models To Admin
Not only does Django create the Model and Migrations for you, but it also gives you a way to register that Model with the Admin Dashboard. Once it is registered in the admin dashboard, you automatically get full CRUD (Create, Read, Update, Delete) ability right in the admin area.
todotodoappadmin.py
from django.contrib import admin
from .models import Todo
# Register your models here.
admin.site.register(Todo)
Just like that, the Todoapp and Todo model is visible in the Admin dashboard.
Let’s add a new record to the database using this Model.
Cool! It is added with a nice success message.
Go ahead and add a few items so that we have some data to work with in the next section.
Working With Function-Based Views
Django has something known as Class Based Views, but in this example, we will use the easier to understand Function-Based Views. In the code below, the first thing we need to do is to import the Todo model we created earlier. With that in place, we define a home function that uses the Django ORM to fetch all todos from the database. Once we have them, we render the home template while also passing the data to that template using the context dictionary.
todotodoappviews.py
from django.shortcuts import render
from .models import Todo
def home(request):
todos = Todo.objects.all()
return render(request, 'todoapp/home.html', {'todos': todos})
Looping Over Items In A Template
This gives us access to all of those todos we just fetched from the database. We can loop over them and output information about each todo like so.
todotodoapptemplatestodoapphome.html
{% extends 'todoapp/base.html' %}
{% block content %}
<section class="section">
<div class="container">
{% for todo in todos %}
<h1 class="title">
{{ todo.task }}
</h1>
{% endfor %}
</div>
</section>
{% endblock %}
Things are taking shape! We now see all of the records from the database getting output to the home page.
Using A Table In Bulma
To spruce things up a bit, we’ll make use of a nice table layout for the todo items using an HTML table styled with Bulma CSS. We can add headers to each column so it is a bit more clear as to what the data is. We have the task to be completed, whether it is completed or not, and the beginnings of a link to delete the todo.
todotodoapptemplatestodoapphome.html
{% extends 'todoapp/base.html' %}
{% block content %}
<section class="section">
<table class="table is-hoverable is-fullwidth">
<thead>
<tr>
<th>Task</th>
<th>Completed</th>
<th>Delete</th>
</tr>
</thead>
{% for todo in todos %}
<tr>
<td>
{{ todo.task }}
</td>
<td>
{{ todo.completed }}
</td>
<td>
Remove Task
</td>
</tr>
{% endfor %}
</table>
</section>
{% endblock %}
Conditional Logic In Templates
We can take various actions in the template based on what the data holds that we are looping over. The code below adds the ability to display a line-through and checkmark on completed items. For incomplete items, we display the normal text along with an empty circle. The icons are made available via Font Awesome. Bulma and Font Awesome kind of work like bread and butter.
todotodoapptemplatestodoapphome.html
{% extends 'todoapp/base.html' %}
{% block content %}
<section class="section">
<table class="table is-hoverable is-fullwidth">
<thead>
<tr>
<th>Task</th>
<th>Completed</th>
<th>Delete</th>
</tr>
</thead>
{% for todo in todos %}
<tr>
<td>
{% if todo.completed == True %}
<span style="text-decoration: line-through">{{ todo.task }}</span>
{% else %}
{{ todo.task }}
{% endif %}
</td>
<td>
{% if todo.completed == True %}
<i class="far fa-check-circle"></i> {{ todo.completed }}
{% else %}
<i class="far fa-circle"></i> {{ todo.completed }}
{% endif %}
</td>
<td>
Remove Task
</td>
</tr>
{% endfor %}
</table>
</section>
{% endblock %}
This provides for a pretty cool effect!
Using Django Forms
Let’s get our form working properly in this Django app. To do so, we need to add a forms.py file to the application directory.
This example here makes use of Django Model Forms. These things are incredible and feel like magic once you get them working :-).
todotodoappforms.py
from django import forms
from .models import Todo
class TodoForm(forms.ModelForm):
class Meta:
model = Todo
fields = ['task', 'completed']
This new Model Form can now be imported in our views.py file. The new TodoForm class uses an object instance to hold any data sent from the HTML form. To populate the form in code, all you need to do is use form = TodoForm(request.POST or None). By reading the other code, you can see how to validate the form, and save it to the database. Note that in Django, a particular view function can handle either a GET or POST request. It is your responsibility to check for what the request type is, then take the appropriate action.
todotodoappviews.py
from django.shortcuts import render
from .models import Todo
from .forms import TodoForm
def home(request):
if request.method == 'POST':
form = TodoForm(request.POST or None)
if form.is_valid():
form.save()
todos = Todo.objects.all()
return render(request, 'todoapp/home.html', {'todos': todos})
else:
todos = Todo.objects.all()
return render(request, 'todoapp/home.html', {'todos': todos})
The form that exists in the base.html file needs to be updated to work with our model. The snippet below shows how we set the method to POST, add a csrf token, and ensure that the properties of the input tag are correct.
todotodoapptemplatestodoappbase.html
<form method="POST">
{% csrf_token %}
<div class="field has-addons">
<div class="control">
<input name="task" class="input" type="text" placeholder="Add Todo">
</div>
<div class="control">
<button type="submit" class="button is-link">Submit</button>
</div>
</div>
</form>
Test Out The Form
We are ready to type a new todo item into the form, then click submit. This should add the todo item and save it to the database.
After clicking on submit, the page reloads with the new todo item added to the table. The form is working!
Adding Flash Messages
Django has a nice messages module that makes it easy to add flash messages to your application. First, we can update the code in the views file to make use of the messages module.
todotodoappviews.py
from django.shortcuts import render, redirect
from .models import Todo
from .forms import TodoForm
from django.contrib import messages
def home(request):
if request.method == 'POST':
form = TodoForm(request.POST or None)
if form.is_valid():
form.save()
todos = Todo.objects.all()
messages.success(request, ('Task has been added!'))
return render(request, 'todoapp/home.html', {'todos': todos})
else:
todos = Todo.objects.all()
return render(request, 'todoapp/home.html', {'todos': todos})
In the template, we can check for the presence of a flash message and if one is present, output the message using the Bulma CSS styling for a nice effect.
todotodoapptemplatestodoapphome.html
{% extends 'todoapp/base.html' %}
{% block content %}
<section class="section">
{% if messages %}
{% for message in messages %}
<article class="message is-success">
<div class="message-header">
<p>Nice!</p>
</div>
<div class="message-body">
{{ message }}
</div>
</article>
{% endfor %}
{% endif %}
<table class="table is-hoverable is-fullwidth">
<thead>
<tr>
<th>Task</th>
<th>Completed</th>
<th>Delete</th>
</tr>
</thead>
{% for todo in todos %}
<tr>
<td>
{% if todo.completed == True %}
<span style="text-decoration: line-through">{{ todo.task }}</span>
{% else %}
{{ todo.task }}
{% endif %}
</td>
<td>
{% if todo.completed == True %}
<i class="far fa-check-circle"></i> {{ todo.completed }}
{% else %}
<i class="far fa-circle"></i> {{ todo.completed }}
{% endif %}
</td>
<td>
Remove Task
</td>
</tr>
{% endfor %}
</table>
</section>
{% endblock %}
Let’s add another task to our list of todos.
Bingo! Now the todo is added, but we also get a nice message that it was added successfully.
Delete An Item By Id
In this section, we can set up the routing, view, and link in the template file to allow for deleting a specific todo task.
todotodoappurls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='home'),
path('delete/<int:todo_id>', views.delete, name='delete'),
]
todotodoappviews.py
from django.shortcuts import render, redirect
from .models import Todo
from .forms import TodoForm
from django.contrib import messages
def home(request):
if request.method == 'POST':
form = TodoForm(request.POST or None)
if form.is_valid():
form.save()
todos = Todo.objects.all()
messages.success(request, ('Task has been added!'))
return render(request, 'todoapp/home.html', {'todos': todos})
else:
todos = Todo.objects.all()
return render(request, 'todoapp/home.html', {'todos': todos})
def delete(request, todo_id):
todo = Todo.objects.get(id=todo_id)
todo.delete()
messages.success(request, ('Task has been Deleted!'))
return redirect('home')
todotodoapptemplatestodoapphome.html
{% extends 'todoapp/base.html' %}
{% block content %}
<section class="section">
{% if messages %}
{% for message in messages %}
<article class="message is-success">
<div class="message-header">
<p>Nice!</p>
</div>
<div class="message-body">
{{ message }}
</div>
</article>
{% endfor %}
{% endif %}
<table class="table is-hoverable is-fullwidth">
<thead>
<tr>
<th>Task</th>
<th>Completed</th>
<th>Delete</th>
</tr>
</thead>
{% for todo in todos %}
<tr>
<td>
{% if todo.completed == True %}
<span style="text-decoration: line-through">{{ todo.task }}</span>
{% else %}
{{ todo.task }}
{% endif %}
</td>
<td>
{% if todo.completed == True %}
<i class="far fa-check-circle"></i> {{ todo.completed }}
{% else %}
<i class="far fa-circle"></i> {{ todo.completed }}
{% endif %}
</td>
<td>
<a href="{% url 'delete' todo.id %}">Remove Task</a>
</td>
</tr>
{% endfor %}
</table>
</section>
{% endblock %}
Now we click one of the links for a specific task, in this case, we click the link for “Wash The Lettuce” to delete that todo.
Just like that, that particular task is deleted.
Mark A Todo As Complete or Incomplete
We also want the ability to mark a task as complete or incomplete. Maybe we don’t want to delete the task entirely but have a running list where you can cross off completed items. We can set that up now.
todotodoappurls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='home'),
path('delete/<int:todo_id>', views.delete, name='delete'),
path('mark_complete/<int:todo_id>', views.mark_complete, name="mark_complete"),
path('mark_incomplete/<int:todo_id>', views.mark_incomplete, name="mark_incomplete"),
]
todotodoappviews.py
from django.shortcuts import render, redirect
from .models import Todo
from .forms import TodoForm
from django.contrib import messages
def home(request):
if request.method == 'POST':
form = TodoForm(request.POST or None)
if form.is_valid():
form.save()
todos = Todo.objects.all()
messages.success(request, ('Task has been added!'))
return render(request, 'todoapp/home.html', {'todos': todos})
else:
todos = Todo.objects.all()
return render(request, 'todoapp/home.html', {'todos': todos})
def delete(request, todo_id):
todo = Todo.objects.get(id=todo_id)
todo.delete()
messages.success(request, ('Task has been Deleted!'))
return redirect('home')
def mark_complete(request, todo_id):
todo = Todo.objects.get(id=todo_id)
todo.completed = True
todo.save()
return redirect('home')
def mark_incomplete(request, todo_id):
todo = Todo.objects.get(id=todo_id)
todo.completed = False
todo.save()
return redirect('home')
todotodoapptemplatestodoapphome.html
{% extends 'todoapp/base.html' %}
{% block content %}
<section class="section">
{% if messages %}
{% for message in messages %}
<article class="message is-success">
<div class="message-header">
<p>Nice!</p>
</div>
<div class="message-body">
{{ message }}
</div>
</article>
{% endfor %}
{% endif %}
<table class="table is-hoverable is-fullwidth">
<thead>
<tr>
<th>Task</th>
<th>Completed</th>
<th>Delete</th>
</tr>
</thead>
{% for todo in todos %}
<tr>
<td>
{% if todo.completed == True %}
<span style="text-decoration: line-through">{{ todo.task }}</span>
{% else %}
{{ todo.task }}
{% endif %}
</td>
<td>
{% if todo.completed == True %}
<i class="far fa-check-circle"></i>
<a href="{% url 'mark_incomplete' todo.id %}">Mark Incomplete</a>
{% else %}
<i class="far fa-circle"></i>
<a href="{% url 'mark_complete' todo.id %}">Mark Complete</a>
{% endif %}
</td>
<td>
<a href="{% url 'delete' todo.id %}">Remove Task</a>
</td>
</tr>
{% endfor %}
</table>
</section>
{% endblock %}
Check out the short video clip below to show this in action now.
Edit A Task
The last piece of functionality we can add is the ability to edit a todo item. The goal is to provide a link for each task description, which then launches a prepopulated form with the data to edit. Then, the user can make any edits they like, click the submit button on the form, and the task will have been updated. Once again, below are the edits to the URLs, views, and templates to get this wired up.
todotodoappurls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='home'),
path('delete/<int:todo_id>', views.delete, name='delete'),
path('mark_complete/<int:todo_id>', views.mark_complete, name="mark_complete"),
path('mark_incomplete/<int:todo_id>', views.mark_incomplete, name="mark_incomplete"),
path('edit/<int:todo_id>', views.edit, name="edit"),
]
todotodoappviews.py
from django.shortcuts import render, redirect
from .models import Todo
from .forms import TodoForm
from django.contrib import messages
def home(request):
if request.method == 'POST':
form = TodoForm(request.POST or None)
if form.is_valid():
form.save()
todos = Todo.objects.all()
messages.success(request, ('Task has been added!'))
return render(request, 'todoapp/home.html', {'todos': todos})
else:
todos = Todo.objects.all()
return render(request, 'todoapp/home.html', {'todos': todos})
def delete(request, todo_id):
todo = Todo.objects.get(id=todo_id)
todo.delete()
messages.success(request, ('Task has been Deleted!'))
return redirect('home')
def mark_complete(request, todo_id):
todo = Todo.objects.get(id=todo_id)
todo.completed = True
todo.save()
return redirect('home')
def mark_incomplete(request, todo_id):
todo = Todo.objects.get(id=todo_id)
todo.completed = False
todo.save()
return redirect('home')
def edit(request, todo_id):
if request.method == 'POST':
todo = Todo.objects.get(id=todo_id)
form = TodoForm(request.POST or None, instance=todo)
if form.is_valid():
form.save()
messages.success(request, ('Task has been edited!'))
return redirect('home')
else:
todo = Todo.objects.get(id=todo_id)
return render(request, 'todoapp/edit.html', {'todo': todo})
todotodoapptemplatestodoappedit.html
{% extends 'todoapp/base.html' %}
{% block content %}
<section class="section">
<form method="POST">
{% csrf_token %}
<div class="field has-addons">
<div class="control">
<input name="task" class="input" type="text" placeholder="{{ todo.task }}" value="{{ todo.task }}">
<input name="completed" type="hidden" value="{{ todo.completed }}">
</div>
<div class="control">
<button type="submit" class="button is-link">Edit Todo</button>
</div>
</div>
</form>
</section>
{% endblock %}
Now, we test this out by clicking on the ‘Edit Task’ link for the “Pick Some Tomatoes” todo item. It loads up a new form with that text, and we change it to “Pick So Many Tomatoes!!”. Once the button is clicked, the edit is complete and the page redirects with a nice flash message to let us know the edit was successful.
Todo App With Django And Bulma Summary
Thanks for reading this fun little tutorial about using Django and Bulma together to build a basic todo application. We saw how to do all of the CRUD operations in Django, along with learning a bit about the cool new CSS framework Bulma.