DEV Community

Cover image for Building a Todo List Application with Flask
Faruq Abdulsalam
Faruq Abdulsalam

Posted on • Edited on

Building a Todo List Application with Flask

In this part of the series, you'll learn how to build a web application with the Flask framework. At the end of this part of the series, you'll have a working application with create, read, and delete functionalities.

Let's get started!!

Database Setup

The database that will be used here is sqlite, but you can choose any db of your choice.

You'll need to install the Flask-SQLAlchemy and Flask-Migrate extensions which will map all the data in your application to the sqlite db and also handle database migrations.



pip install flask-sqlalchemy


Enter fullscreen mode Exit fullscreen mode


pip install flask-migrate


Enter fullscreen mode Exit fullscreen mode

In the root directory, create a config.py and a .env file where all the configurations for the application will be stored.



touch config.py


Enter fullscreen mode Exit fullscreen mode


touch .env


Enter fullscreen mode Exit fullscreen mode

.env

First, set up the .env file by adding your secret key here. Remember that it is bad practice to hardcode your SECRET KEY in the main body of your application as this would make your application vulnerable to security breaches in production. That is why you need to always set it up as an environmental variable.



SECRET_KEY = put-your-own-secret-secret-key-here


Enter fullscreen mode Exit fullscreen mode

Config.py

Then add your configurations:



import os
from dotenv import load_dotenv 

load_dotenv()
basedir = os.path.abspath(os.path.dirname(__file__))

class Configuration(object):
    SECRET_KEY = os.environ.get('SECRET_KEY')
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'todo.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False


Enter fullscreen mode Exit fullscreen mode

Here os is imported to handle the paths and also retrieve data from the environment.

The load_dotenv function is imported from the installed dotenv package and then called. This loads the SECRET KEY variable stored in the .env file.



basedir = os.path.abspath(os.path.dirname(__file__))


Enter fullscreen mode Exit fullscreen mode

The value assigned to the basedir variable is the path for the root directory. This is done to avoid hardcoding the path to the root directory which may change whenever you make changes to the structure of your directories.

Finally, the configuration settings are assigned to variables under the Configuration class.

i) the value of the SECRET KEY obtained when the load_dotenv function is called and assigned to the SECRET KEY variable.

ii) the database URL is obtained from the environment file and assigned to the SQLALCHEMY_DATABASE_URI variable. The or operator is used here so that if the database URL is not specified in the .env file, the variable DATABASE URL is set to the base directory. todo.db is the name of the database that'll be created in the root directory. You can choose to change the name of the db to whatever suits you.

iii) SQLALCHEMY_TRACK_MODIFICATIONS is set to false, this disables the feature which sends a signal every time the database changes.

The database and configuration setup is done. Now, you need to register it in your init.py script.

init.py



from flask import Flask
from config import Configuration
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
app.config.from_object(Configuration)
db = SQLAlchemy(app)
migrate = Migrate(app, db)

from core import views, models


Enter fullscreen mode Exit fullscreen mode

Import the Configuration class and configure the app variable with it. This assigns all the configuration settings to the application.

Next, import SQLAlchemy and Migrate from the packages installed earlier and configure them as done above.

models.py

You need to create a database module models.py in your core directory.



from core import db

class Todo(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(140))
    date = db.Column(db.Date())
    time = db.Column(db.Time())
    category= db.Column(db.String, db.ForeignKey('category.id'))

    def __repr__(self):
        return '<ToDo {}>'.format(self.title)


Enter fullscreen mode Exit fullscreen mode

The db variable in the __init__.py file is imported here and used to set up the structure(columns) of the database.

The id field is declared as an integer type and the database will assign the primary key automatically.

The Todolist database model class will have title, date, and time fields as well so the title field is declared as a string type, date and time are declared as types date and time respectively.

Each todo created will also have a category, so you need to create a category field as type string. This will be connected to the Category database model that will be created later on.

The last line def __repr__(self): defines the format in which python will print the objects of this database model.

Blueprints

The application is about to grow massively, to prevent the project from becoming rough and confusing, and to ensure separation of concerns we'll be using Blueprints. Each part of the application will be made into modular applications which will all be registered in the __init__.py file in the core directory.

Let's start with the blueprint (mini-application) that will make it possible for the user to create todos. Create a new directory task in the core directory. Next, create the following files in the task directory: __init__.py, forms.py, models.py, and views.py.

Remember that this is going to be an application on its own so you need to create a templates folder in the task directory as well. Create a new folder task in it and within it a file named tasks.html.

task blueprint

Let's work on the init.py script.

init.py



from flask import Blueprint

task = Blueprint('task', __name__, template_folder='templates')

from . import views


Enter fullscreen mode Exit fullscreen mode

The Blueprint class is imported from the flask package and the name of the current blueprint task is assigned to the constructor Blueprint(). The name is passed as an argument as well, as discussed earlier python automatically passes the name of the base module core here. Next, the folder in which the application will look for template files is assigned to the template_folder variable and also passed as an argument to the blueprint constructor.

Finally, the content of the views.py file will be imported into the script so that they can all be registered with the blueprint.

models.py



from .. import db

class Category(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20))

    def __repr__(self):
        return '<Category {}>'.format(self.name)


Enter fullscreen mode Exit fullscreen mode

The db variable is imported from the core directory and used to set up the Category database model class. This should be self-explanatory with the explanation done above when the Todo model class was created.

forms.py

To create the forms for the application you'll need to install Flask-WTF:



pip install flask-wtf


Enter fullscreen mode Exit fullscreen mode

Then, you can create the various fields for the form that users will use to create todos.



from flask_wtf import FlaskForm
from wtforms import StringField, TimeField, DateField, SubmitField, SelectField
from wtforms.validators import DataRequired, Length, Email, EqualTo

class TaskForm(FlaskForm):
    title = StringField('Title', validators=[DataRequired()])
    category = SelectField('Category', coerce=int , validators=[DataRequired()])
    date = DateField('Date', format='%Y-%m-%d' , validators=[DataRequired()])
    time = TimeField('Time', format='%H:%M' , validators=[DataRequired()])
    submit = SubmitField('Add task')


Enter fullscreen mode Exit fullscreen mode

A label is the first variable assigned to each form field declared, DataRequired is passed to the form validators as well to ensure that the fields are not left blank by the user.

The coerce keyword arg passed to the SelectField tells the application to use the int() function to coerce data for this particular field from the Task form.

views.py



from flask import render_template, redirect, url_for, request
from .models import Category
from ..models import Todo
from . import task
from .forms import TaskForm
from .. import db
from datetime import datetime

@task.route('/create-task', methods=['GET', 'POST'])
def tasks():
    check= None
    todo= Todo.query.all()
    date= datetime.now()
    now= date.strftime("%Y-%m-%d")

    form= TaskForm()
    form.category.choices =[(category.id, category.name) for category in Category.query.all()]

    return render_template('task/tasks.html', title='Create Tasks', form=form, todo=todo, DateNow=now, check=check)



Enter fullscreen mode Exit fullscreen mode

First, the required functions from flask, model classes, db, form, and datetime class are imported into the views file.

The variable task is imported from the __init__.py blueprint script and is then extended as shown below with .route functions by mapping the URL and adding the HTTP methods which in this case are GET and POST.



@task.route('/create-task', methods=['GET', 'POST'])


Enter fullscreen mode Exit fullscreen mode

This is used as a decorator for the tasks function defined below. In part 1 of the series app.route was used but since the task blueprint is being used here, you can simply use task.route since the blueprint will eventually be registered in the app.

The check variable initially assigned a value of None is simply used to pass an error message to the template. Next, the Todo table in the database is queried for all the todos in the database. This would later be changed when the authentication blueprint is set up. The present date is obtained and formatted using the strftime function.

Finally, the view uses render_template function to render the tasks.html file in the tasks directory located in the templates directory. The function also takes the title, form, DateNow, and check arguments that it will use to replace the placeholder variables that will be added to the task html file.

The choices for the select field are assigned the result of querying the Category table in the database. Let's test the application before adding the conditions that will handle the methods.

init.py

The task blueprint needs to be registered with the application so its content can also be recognized when the application is being run. Open the __init__.py script in the core directory and add the following lines before the last import line:



# blueprint for non-authentication parts of the app
from .task import task as task_blueprint
app.register_blueprint(task_blueprint)


Enter fullscreen mode Exit fullscreen mode

The task variable to which the blueprint is assigned is imported from the task directory as task_blueprint and then registered with the application using the Flask inbuilt register_blueprint function.

Now you have to migrate the new database models to the database; you'll notice two new additions to your directory after doing this.

i) a new folder named migrations will be added to your root directory.

ii) a new database will also be created in your root directory bearing whatever name you specified in your config file. If you remember, the name of the db was specified as todo.db earlier on. If you made any change to yours, the database that will be created will bear the name you specified as well. Now let's see this in action. Run:



flask db init


Enter fullscreen mode Exit fullscreen mode

You should see the new folder migrations added to your root directory. Next, run:



flask db migrate -m "Put any comment of your choice here"


Enter fullscreen mode Exit fullscreen mode

Now you should see the created database todo.db in your root directory as well.

The last command to run is:



flask db upgrade


Enter fullscreen mode Exit fullscreen mode

This will apply all the changes you made to the database model class to your database. Your directory should currently look like 👇.
Directory look

The category table in the database is currently empty so let's go ahead and create the three categories that the user can choose when creating their todo list.

python shell

Open up the python shell in your terminal using:



python


Enter fullscreen mode Exit fullscreen mode

Then, import the Category model from the task directory in the core directory and the db as well



from core.task.models import Category
from core import db 


Enter fullscreen mode Exit fullscreen mode

Create the three categories



ctgry1 = Category(name='Business')
ctgry2 = Category(name='Personal')
ctgry3 = Category(name='Other')


Enter fullscreen mode Exit fullscreen mode

Now, the variables need to be added to the current database session using:



db.session.add(ctgry1)
db.session.add(ctgry2)
db.session.add(ctgry3)


Enter fullscreen mode Exit fullscreen mode

Finally, the session is committed so that the changes are applied to the Category table in the database.



db.session.commit()


Enter fullscreen mode Exit fullscreen mode

To confirm that the categories Business, Personal, and Other have all been added to the Category table in the database, run the commands below.



categories = Category.query.all()
for c in categories:
    print (c.name)


Enter fullscreen mode Exit fullscreen mode

The categories should be printed out exactly like what is in the image below.
Categories List

Before you can go ahead and run the application, you need to create the HTML code that will be rendered by the task view function.
First you need to create a base html file and then set up the task html file as an extension of the base file.

base.html

Create a new file named base.html inside the templates directory in the core directory.



<!doctype html>
<html>
    <head>

      {% if title %}
      <title>{{ title }} </title>
      {% else %}
      <title>Todo List</title>
      {% endif %}
    </head>
    <body>

        {% block content %} {% endblock content%}

    </body>
</html>


Enter fullscreen mode Exit fullscreen mode

If you are new to jinja2 templates, the curly braces might look odd. The variable declared in the curly braces gets passed the value assigned to the variable when the render_template function is called in the view. For example, Create Tasks has already been assigned to the title variable so whenever the render_template function is executed, {{title}} in the tasks.html file gets replaced with Create Tasks. In jinja templates you can also make use of conditions such as if and for.

Every other html file in the application will be an extension of this base.html file and their contents will be filled into {% block content %} {% endblock content%}.

tasks.html



{% extends "base.html" %}

{% block content %}

<div class="task_content categories" >    
    <div class="">
        <div class="content">
            <div class="welcome">
                <button id="openbutton" class="openbtn" >&#9776;</button>
                <span class="welcome-text">Manage Your ToDo List</span> 
            </div>

            <form action="" method="post" novalidate>
                {{ form.hidden_tag() }}
                <div class="inputContainer Task">
                    {{ form.title.label }}<br>
                    {{ form.title(size=20) }}
                    {% for error in form.title.errors %}
                    <span style="color: red;">[{{ error }}]</span>
                    {% endfor %}
                </div>
                <div class="inputContainer choice ">
                    {{ form.category.label}}<br>
                    {{ form.category}}
                    {% for error in form.category.errors %}
                    <span style="color: red;">[{{ error }}]</span>
                    {% endfor %}
                </div>
                <div class="inputContainer  date ">
                    {{ form.date.label }}<br>
                    {{ form.date }}
                    {% for error in form.date.errors %}
                    <span style="color: red;">[{{ error }}]</span>
                    {% endfor %}
                </div>
                <div class="inputContainer  time">
                    {{ form.time.label }}<br>
                    {{ form.time }}
                    {% for error in form.time.errors %}
                    <span style="color: red;">[{{ error }}]</span>
                    {% endfor %}
                </div>
                <div class="buttons">   
                    {{ form.submit(class="taskAdd btn") }}
                </div> 
            </form>
            <!-- Task Delete Error alert -->
            {% if check %}
                <div class="alert alert-warning" role="alert">
                    <span class="closebtns" onclick="this.parentElement.style.display='none';">&times;</span>
                    {{check}}
                </div>
            {% endif %}
            <!-- End Task Delete error alert -->

            <form action="" method="post">
              {{ form.hidden_tag() }} <!-- csrf token for security -->

              <div class="tabs effect-1">
                 <!-- tab-content -->
                <div class="tab-content">
                  <section id="tab-item-1">
                      <ul class="taskList">        
                          {% for todo in todo %} 
                          <li class="currentTaskItem">
                              <input type="checkbox" class="taskCheckbox" name="checkedbox" id="{{ todo.id }}" value="{{ todo.id }}" >
                              <label for="{{ todo.id }}"><span class="complete-">{{ todo.title }}</span></label>
                              <span class="taskDone" >at</span>
                              <strong class="taskDone"><i class="fa fa-clock-o"></i> {{ todo.time }}</strong>
                              <span class="taskDone" >on</span>
                              <strong class="taskDatee taskDone"><i class="fa fa-calendar"></i> {{ todo.date }}</strong>
                              <span class="categorypage-{{ todo.category }}">{{ todo.category }}</span>
                              <button class="taskDelete " name="taskDelete" formnovalidate="" type="submit" ><i class="fa fa-trash-o icon"></i></button>
                          </li>     
                          {% endfor %}
                      </ul> <!--end All-Tasks-List -->
                  </section>
                </div><!-- end tab content -->
              </div><!-- end tab effect -->
            </form>
        </div><!-- end content -->

    </div><!-- container -->
</div>
{% endblock %}


Enter fullscreen mode Exit fullscreen mode

The form.hidden_tag() gets the csrf token which protects the form against cross site request forgery(csrf) attacks.

In the first form in the HTML file, the labels set when creating the TaskForm e.g {{ form.title.label }} are used as the labels for the input elements. While the input elements are created using the fields created in the form e.g {{ form.title(size=20) }}.

The size=20 argument is an example of how to pass classes/attributes to the form field inputs. This sets the width of the input field to 20.

The if check line confirms if an error message exists and then renders the error alert elements if it does. Otherwise, no error message is displayed.

In the second form, you first need to confirm that a todo list exists from the database query made in the view. Then, the result is looped through to get the title, time, date, and category variables and displayed as li elements accordingly.

The action attributes in the forms are left blank because they'll both be sending form data to the page url.

Now let's see what has been built so far in the web browser. Run:



flask run


Enter fullscreen mode Exit fullscreen mode

Your web browser should still be displaying Hello there, Ace. Navigate to http://127.0.0.1:5000/create-task and you'll see the page below.
Task creation page
Note that the title of the webpage was updated to what was specified in the render template function; title='Create Tasks' and the form was rendered as a normal form even though you didn't exactly create form inputs in the HTML file. Flask-WTF handles this for you.

Now, let's finish up the task view so that it can handle POST data from the front end. Add the following conditions to the task function.



    if request.method == "POST":
        if request.form.get('taskDelete') is not None:
            deleteTask = request.form.get('checkedbox')
            if deleteTask is not None:
                todo = Todo.query.filter_by(id=int(deleteTask)).one()
                db.session.delete(todo)
                db.session.commit()
                return redirect(url_for('task.tasks'))
            else:
                check = 'Please check-box of task to be deleted'

        elif form.validate_on_submit():
            selected= form.category.data
            category= Category.query.get(selected)
            todo = Todo(title=form.title.data, date=form.date.data, time= form.time.data, category= category.name)
            db.session.add(todo)
            db.session.commit()
            return redirect(url_for('task.tasks'))


Enter fullscreen mode Exit fullscreen mode

All the logic here will only be run if the method is a POST request. Otherwise, the empty form will be rendered once again without any data. The if condition contains two further conditions to check whether the user wants to delete or create a todo.

Each todo will be rendered as a form with a delete button attached to it so if a post request is made the first condition checks if it was one of the delete forms that was submitted using the name attribute taskDelete of the delete form's submit button specified in the task template file.

If the condition passes, the next condition confirms that the checkbox was also checked before the Todo table in the database is queried by filtering with the id of the particular todo whose delete button was clicked. .one() at the end of the query, limits the number of rows returned from the database to one and that would be only the todo with the specified id. If the checkbox was not checked before the submit(Delete button) was pressed, an error message is displayed notifying the user that it needs to be checked before the todo can be deleted.

Then just like you did when the categories were created in the python shell, you also need to pass the todo object obtained from the database to the session and then commit it so that the changes will be applied to the database. After this is done, the page is made to refresh with the redirect function by passing the same view function tasks as an argument. task.tasks is used here since you are working in the task blueprint and this lets the application know the path to the task view.

However if the first condition evaluates to false, the second condition is executed and the form data is extracted after the data submitted by the user has been validated. The selected category comes back as an integer value(the id) not a string since the coerce argument was set to int() for the selectField in the forms.py file. The id value is then used to query the Category table in the database to get its matching Category in the db. Next, all the data passed from the form is used to generate the object passed to the todo variable, passed to the session, and finally committed to the database for storage. Thus, creating a new todo in the database. When this is completed, the page is made to refresh as well with the redirect function by passing the same view function tasks as an argument.

Your task function should look exactly like 👇
Task function view

The create and delete functions have been created so you can put them to test now. Run your application:



flask run


Enter fullscreen mode Exit fullscreen mode

Fill the task form and click on the Add Task button. Upon submission you'll notice that the web page reloads, the form is reset to default and your new task gets displayed.
Task created
The button circled in red in the picture above is the delete button, click on it to delete the created todo. You'll get the error message specified in the task view which is to notify the user that the checkbox needs to be checked. This helps to introduce a form of control for the delete button, making it impossible for the user to delete a created todo by mistake. Check the box of the todo and click on the delete button once again. Now you'll notice that the todo gets deleted.

Congratulations!! You have successfully created your first FLASK application with create, read, and delete features. This is the end of part 2 of the Flask series. Authentication will be added to the application in part 3. Cheers!!!

If you have any questions, feel free to drop them as a comment or send me a message on Linkedin or Twitter and I'll ensure I respond as quickly as I can. Ciao 👋

Top comments (6)

Collapse
 
nimishagupta profile image
Nimisha Gupta

I am running in the following runtime error: RuntimeError: Working outside of application context.

db.session.add(ctgry1)
Traceback (most recent call last):
File "", line 1, in
File "/home/ubuntu/flask/env/lib/python3.10/site-packages/sqlalchemy/orm/scoping.py", line 361, in add
return self.proxied.add(instance, _warn=_warn)
File "/home/ubuntu/flask/env/lib/python3.10/site-packages/sqlalchemy/orm/scoping.py", line 188, in _proxied
return self.registry()
File "/home/ubuntu/flask/env/lib/python3.10/site-packages/sqlalchemy/util/_collections.py", line 639, in __call
_
key = self.scopefunc()
File "/home/ubuntu/flask/env/lib/python3.10/site-packages/flask_sqlalchemy/session.py", line 102, in _app_ctx_id
return id(app_ctx._get_current_object()) # type: ignore[attr-defined]
File "/home/ubuntu/flask/env/lib/python3.10/site-packages/werkzeug/local.py", line 513, in _get_current_object
raise RuntimeError(unbound_message) from None
RuntimeError: Working outside of application context.


This typically means that you attempted to use functionality that needed
the current application. To solve this, set up an application context
with app.app_context(). See the documentation for more information.

Collapse
 
nimishagupta profile image
Nimisha Gupta

This is resolved with app.app_context().push()

Collapse
 
benjaminkucler1 profile image
benjaminkucler1

I had problem with TemplateNotFoundError. My structure was /core/task/templates/tasks.html.

When I fixed structure to /core/task/templates/task/tasks.html as the internet suggested, It started to work normally.

Is this something new? Considering that you didn't have to insert another task folder in templates.

Thank you

Collapse
 
jhersbo profile image
Jack H. Ersbo

I believe the author had their tasks.html file within a template/task directory. Change the argument passed to render_template in task/views.py from 'task/tasks.html' to 'tasks.html' and it should work.

Collapse
 
capcom6 profile image
capcom6

Category name is not unique, is it normal to use it in foreign key? What about using Category.name as primary key instead of id?

Collapse
 
nagatodev profile image
Faruq Abdulsalam

Hello capcom,

Thanks for your observation. I have made the change to the User model. I decided to use Category.Id with the foreign key instead.

Thanks for reading