Todo App With Django And Bulma

Django and Bulma

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.

django installed successfully

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.

django built in admin login

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 $

django superuser admin login

Use the credentials you provided when creating your superuser to log in to the admin area and have a look around.

django admin logged in


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.

django startapp files


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.

django add urlspy to app

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.

django templates in 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.

first web page django


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.

django base_html file


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!

base template extends django


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.

bulma in django base template


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.

python managepy makemigrations

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.

register model in django admin

Let’s add a new record to the database using this Model.

add items to django admin

Cool! It is added with a nice success message.

view new items in django admin

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.

updated django 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 %}

bulma css table

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!

bulma font awesome checkmark


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.

add formspy to django app

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.

django form submit

After clicking on submit, the page reloads with the new todo item added to the table. The form is working!

the django form submit worked

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.

add another todo django

Bingo! Now the todo is added, but we also get a nice message that it was added successfully.

django success flash message


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.

add link for delete django item

Just like that, that particular task is deleted.

django delete by id


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.