20
loading...
This website collects cookies to deliver better user experience
Flask
framework. At the end of this part of the series, you'll have a working application with create
, read
, and delete
functionalities.sqlite
, but you can choose any db of your choice.sqlite
db and also handle database migrations.pip install flask-sqlalchemy
pip install flask-migrate
config.py
and a .env
file where all the configurations for the application will be stored.touch config.py
touch .env
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
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
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__))
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.Configuration
class.SECRET KEY
obtained when the load_dotenv
function is called and assigned to the SECRET KEY
variable.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.SQLALCHEMY_TRACK_MODIFICATIONS
is set to false, this disables the feature which sends a signal every time the database changes.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
Configuration
class and configure the app variable with it. This assigns all the configuration settings to the application.SQLAlchemy
and Migrate
from the packages installed earlier and configure them as done above. 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)
__init__.py
file is imported here and used to set up the structure(columns) of the database.id
field is declared as an integer type and the database will assign the primary key automatically.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.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.def __repr__(self):
defines the format in which python will print the objects of this database model.__init__.py
file in the core directory.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
. templates
folder in the task
directory as well. Create a new folder task in it and within it a file named tasks.html
.from flask import Blueprint
task = Blueprint('task', __name__, template_folder='templates')
from . import views
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.views.py
file will be imported into the script so that they can all be registered with the blueprint.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)
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.pip install flask-wtf
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')
DataRequired
is passed to the form validators as well to ensure that the fields are not left blank by the user.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.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)
views
file.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'])
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.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. 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.Category
table in the database. Let's test the application before adding the conditions that will handle the methods. 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)
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.migrations
will be added to your root directory.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
migrations
added to your root directory. Next, run:flask db migrate -m "Put any comment of your choice here"
todo.db
in your root directory as well.flask db upgrade
todo list
.python
from core.task.models import Category
from core import db
ctgry1 = Category(name='Business')
ctgry2 = Category(name='Personal')
ctgry3 = Category(name='Other')
db.session.add(ctgry1)
db.session.add(ctgry2)
db.session.add(ctgry3)
db.session.commit()
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)
task
view function. task
html file as an extension of the base file.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>
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
. base.html
file and their contents will be filled into {% block content %} {% endblock content%}
.{% extends "base.html" %}
{% block content %}
<div class="task_content categories" >
<div class="">
<div class="content">
<div class="welcome">
<button id="openbutton" class="openbtn" >☰</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';">×</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 %}
form.hidden_tag()
gets the csrf token
which protects the form against cross site request forgery(csrf) attacks.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) }}
.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. 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. 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. action
attributes in the forms are left blank because they'll both be sending form data to the page url. flask run
Hello there, Ace
. Navigate to http://127.0.0.1:5000/create-task
and you'll see the page below.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.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'))
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.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. 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.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. 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.flask run
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
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.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!!!