Flask: Creating a Task Management App
Building a task management application with Flask, a lightweight Python web framework powered by Werkzeug and Jinja2, is an excellent way to create a dynamic, user-friendly platform for organizing tasks. This tutorial guides you through developing a task management app with features like task creation, editing, deletion, and user authentication. It covers setup, key components, and best practices for structuring a maintainable Flask project.
01. Why Build a Task Management App with Flask?
Flask’s simplicity and flexibility make it ideal for building a task management app. It supports modular design with blueprints, integrates seamlessly with databases like SQLAlchemy, and uses Jinja2 for dynamic templating. A Flask task management app can handle user authentication, task CRUD operations, and custom styling while remaining lightweight and extensible.
Example: Basic Task App Setup
# app/__init__.py
from flask import Flask
def create_app():
"""Initialize the Flask task management application."""
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret-key'
@app.route('/')
def home():
"""Render the app homepage."""
return 'Welcome to the Flask Task Manager!'
return app
Run Command:
export FLASK_APP=app
flask run
Output: (In browser at http://127.0.0.1:5000
)
Welcome to the Flask Task Manager!
Explanation:
create_app
- Uses an application factory for modularity.- Sets up the foundation for a task management app.
02. Key Components of the Task Management App
A Flask task management app includes routes for task CRUD operations, a database for storing tasks and users, templates for rendering pages, and user authentication. The table below summarizes key components and their purposes:
Component | Description | Tool/Feature |
---|---|---|
Routes | Handle task creation, editing, deletion | Blueprints, Werkzeug routing |
Database | Store tasks and users | Flask-SQLAlchemy |
Templates | Render dynamic HTML | Jinja2 |
Authentication | Manage user login | Flask-Login |
Forms | Handle user input | Flask-WTF |
2.1 Project Structure
Example: Task App Project Layout
task_manager/
├── app/
│ ├── __init__.py
│ ├── config.py
│ ├── models/
│ │ ├── __init__.py
│ │ ├── task.py
│ │ ├── user.py
│ ├── routes/
│ │ ├── __init__.py
│ │ ├── tasks.py
│ │ ├── auth.py
│ ├── templates/
│ │ ├── base.html
│ │ ├── home.html
│ │ ├── task.html
│ │ ├── create_task.html
│ │ ├── login.html
│ ├── static/
│ │ ├── css/
│ │ │ ├── style.css
│ ├── forms.py
├── tests/
│ ├── test_routes.py
├── requirements.txt
├── run.py
Explanation:
- Organizes code into models, routes, templates, and static files.
- Separates authentication and task routes for modularity.
2.2 Setting Up the Application
Example: Application Factory with Extensions
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from app.routes.tasks import tasks_bp
from app.routes.auth import auth_bp
db = SQLAlchemy()
login_manager = LoginManager()
def create_app():
"""Initialize the Flask task management application."""
app = Flask(__name__)
app.config.from_object('app.config.DevelopmentConfig')
db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
app.register_blueprint(tasks_bp)
app.register_blueprint(auth_bp)
with app.app_context():
db.create_all()
return app
# app/config.py
class DevelopmentConfig:
SECRET_KEY = 'dev-key'
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///tasks.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
Example run.py:
# run.py
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run()
Explanation:
- Initializes SQLAlchemy for database management and Flask-Login for authentication.
- Registers blueprints for task and auth routes.
2.3 Database Models
Example: Task and User Models
# app/models/user.py
from app import db, login_manager
from flask_login import UserMixin
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
password = db.Column(db.String(100), nullable=False)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# app/models/task.py
from app import db
from datetime import datetime
class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
completed = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
user = db.relationship('User', backref='tasks')
Explanation:
User
- Stores user data with authentication support via Flask-Login.Task
- Stores task details with a relationship to the owner.
2.4 Forms for User Input
Example: Login and Task Creation Forms
# app/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, TextAreaField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=50)])
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Login')
class TaskForm(FlaskForm):
title = StringField('Title', validators=[DataRequired(), Length(max=100)])
description = TextAreaField('Description')
completed = BooleanField('Completed')
submit = SubmitField('Save Task')
Explanation:
- Flask-WTF - Simplifies form handling with validation.
- Forms ensure secure and validated user input for tasks and login.
2.5 Authentication Routes
Example: User Login
# app/routes/auth.py
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required
from app.forms import LoginForm
from app.models.user import User
import hashlib
auth_bp = Blueprint('auth', __name__, template_folder='templates')
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
"""Handle user login."""
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and user.password == hashlib.sha256(form.password.data.encode()).hexdigest():
login_user(user)
return redirect(url_for('tasks.home'))
flash('Invalid credentials')
return render_template('login.html', form=form)
@auth_bp.route('/logout')
@login_required
def logout():
"""Handle user logout."""
logout_user()
return redirect(url_for('tasks.home'))
Template: login.html
{% extends 'base.html' %}
{% block content %}
<h2>Login</h2>
{% for message in get_flashed_messages() %}
<p style="color: red;">{{ message }}</p>
{% endfor %}
<form method="post">
{{ form.hidden_tag() }}
{{ form.username.label }} {{ form.username() }}<br>
{{ form.password.label }} {{ form.password() }}<br>
{{ form.submit() }}
</form>
{% endblock %}
Explanation:
- Flask-Login - Manages user sessions for authentication.
- Password hashing ensures secure authentication.
2.6 Task Routes
Example: Task CRUD Routes
# app/routes/tasks.py
from flask import Blueprint, render_template, redirect, url_for, request
from flask_login import login_required, current_user
from app import db
from app.models.task import Task
from app.forms import TaskForm
tasks_bp = Blueprint('tasks', __name__, template_folder='templates')
@tasks_bp.route('/')
def home():
"""Render the task homepage with user's tasks."""
tasks = Task.query.filter_by(user_id=current_user.id).order_by(Task.created_at.desc()).all() if current_user.is_authenticated else []
return render_template('home.html', tasks=tasks)
@tasks_bp.route('/task/<int:task_id>')
def view_task(task_id):
"""Render a single task."""
task = Task.query.get_or_404(task_id)
if task.user_id != current_user.id:
return 'Unauthorized', 403
return render_template('task.html', task=task)
@tasks_bp.route('/create', methods=['GET', 'POST'])
@login_required
def create_task():
"""Handle task creation."""
form = TaskForm()
if form.validate_on_submit():
task = Task(
title=form.title.data,
description=form.description.data,
user_id=current_user.id
)
db.session.add(task)
db.session.commit()
return redirect(url_for('tasks.home'))
return render_template('create_task.html', form=form)
@tasks_bp.route('/edit/<int:task_id>', methods=['GET', 'POST'])
@login_required
def edit_task(task_id):
"""Handle task editing."""
task = Task.query.get_or_404(task_id)
if task.user_id != current_user.id:
return 'Unauthorized', 403
form = TaskForm(obj=task)
if form.validate_on_submit():
task.title = form.title.data
task.description = form.description.data
task.completed = form.completed.data
db.session.commit()
return redirect(url_for('tasks.home'))
return render_template('create_task.html', form=form, task=task)
@tasks_bp.route('/delete/<int:task_id>', methods=['POST'])
@login_required
def delete_task(task_id):
"""Handle task deletion."""
task = Task.query.get_or_404(task_id)
if task.user_id != current_user.id:
return 'Unauthorized', 403
db.session.delete(task)
db.session.commit()
return redirect(url_for('tasks.home'))
Template: home.html
{% extends 'base.html' %}
{% block content %}
<h2>My Tasks</h2>
{% if tasks %}
<ul>
{% for task in tasks %}
<li>
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}">{{ task.title }}</a>
{% if task.completed %} (Completed) {% endif %}
- <a href="{{ url_for('tasks.edit_task', task_id=task.id) }}">Edit</a>
<form method="post" action="{{ url_for('tasks.delete_task', task_id=task.id) }}" style="display:inline;">
<button type="submit" onclick="return confirm('Are you sure?')">Delete</button>
</form>
</li>
{% endfor %}
</ul>
{% else %}
<p>No tasks yet.</p>
{% endif %}
{% if current_user.is_authenticated %}
<a href="{{ url_for('tasks.create_task') }}">Create Task</a>
{% else %}
<a href="{{ url_for('auth.login') }}">Login to manage tasks</a>
{% endif %}
{% endblock %}
Template: base.html
<!DOCTYPE html>
<html>
<head>
<title>Task Manager</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav>
<a href="{{ url_for('tasks.home') }}">Home</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('auth.logout') }}">Logout</a>
{% else %}
<a href="{{ url_for('auth.login') }}">Login</a>
{% endif %}
</nav>
<div>
{% block content %}{% endblock %}
</div>
</body>
</html>
Template: create_task.html
{% extends 'base.html' %}
{% block content %}
<h2>{% if task %}Edit Task{% else %}Create Task{% endif %}</h2>
<form method="post">
{{ form.hidden_tag() }}
{{ form.title.label }} {{ form.title() }}<br>
{{ form.description.label }} {{ form.description() }}<br>
{{ form.completed.label }} {{ form.completed() }}<br>
{{ form.submit() }}
</form>
{% endblock %}
Explanation:
- Routes handle task listing, viewing, creation, editing, and deletion.
- Jinja2 templates render dynamic content with authentication checks.
- Authorization ensures users only access their own tasks.
2.7 Styling
Example: Basic CSS
/* app/static/css/style.css */
body {
font-family: Arial, sans-serif;
margin: 20px;
}
nav {
margin-bottom: 20px;
}
nav a {
margin-right: 10px;
}
h2 {
color: #333;
}
ul {
list-style-type: none;
padding: 0;
}
li {
margin: 10px 0;
}
Explanation:
- Static CSS enhances the app’s appearance.
- Linked in
base.html
for consistent styling.
2.8 Testing
Example: Testing Routes
# tests/test_routes.py
import pytest
from app import create_app, db
from app.models.user import User
from app.models.task import Task
import hashlib
@pytest.fixture
def client():
app = create_app()
app.config['TESTING'] = True
with app.test_client() as client:
with app.app_context():
db.create_all()
yield client
db.drop_all()
def test_create_task(client):
# Create a test user
password = hashlib.sha256('password'.encode()).hexdigest()
user = User(username='testuser', password=password)
db.session.add(user)
db.session.commit()
# Log in
client.post('/login', data={'username': 'testuser', 'password': 'password'})
# Create a task
response = client.post('/create', data={'title': 'Test Task', 'description': 'Do this'})
assert response.status_code == 302 # Redirect to home
assert Task.query.filter_by(title='Test Task').first() is not None
Output: (When running pytest
)
collected 1 item
tests/test_routes.py . [100%]
Explanation:
- Tests validate task creation and database integration.
- Ensures reliability of core app features.
2.9 Incorrect Practices
Example: Monolithic Task App
# app.py
from flask import Flask, render_template, request
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tasks.db'
# All routes, models, and logic in one file
@app.route('/')
def home():
return render_template('home.html')
if __name__ == '__main__':
app.run()
Explanation:
- Monolithic structure is hard to maintain and scale.
- Solution: Use blueprints, separate models, and modular design.
03. Effective Usage
3.1 Recommended Practices
- Use blueprints, application factories, and testing for maintainability.
Example: Complete Task App Setup
# app/__init__.py (as shown in 2.2)
# app/routes/tasks.py (as shown in 2.6)
# app/routes/auth.py (as shown in 2.5)
# app/models/task.py, user.py (as shown in 2.3)
# app/forms.py (as shown in 2.4)
# app/templates/base.html, home.html, etc. (as shown in 2.6)
# Requirements
# requirements.txt
Flask==3.0.3
Flask-SQLAlchemy==3.1.1
Flask-Login==0.6.3
Flask-WTF==1.2.1
pytest==8.3.3
Setup Command:
pip install -r requirements.txt
export FLASK_APP=run
flask run
- Modular design with blueprints and separate concerns.
- Tests and documentation ensure reliability and clarity.
- Secure authentication and form validation.
3.2 Practices to Avoid
- Avoid storing plain-text passwords or skipping authorization.
Example: Insecure Task Access
# app/routes/tasks.py
@tasks_bp.route('/task/<int:task_id>')
def view_task(task_id):
task = Task.query.get_or_404(task_id)
return render_template('task.html', task=task) # No authorization
- Lack of authorization allows access to other users’ tasks.
- Solution: Check
task.user_id == current_user.id
.
04. Common Use Cases
4.1 Displaying User Tasks
Render a list of tasks specific to the logged-in user.
Example: Task Listing
# app/routes/tasks.py (as shown in 2.6)
@tasks_bp.route('/')
def home():
tasks = Task.query.filter_by(user_id=current_user.id).order_by(Task.created_at.desc()).all() if current_user.is_authenticated else []
return render_template('home.html', tasks=tasks)
Output: (In browser at /
)
My Tasks
Explanation:
- Dynamic rendering with Jinja2 displays user-specific tasks.
- Links for editing and deleting enhance usability.
4.2 Secure Task Management
Allow authenticated users to create, edit, and delete tasks securely.
Example: Task Creation
# app/routes/tasks.py (as shown in 2.6)
@tasks_bp.route('/create', methods=['GET', 'POST'])
@login_required
def create_task():
form = TaskForm()
if form.validate_on_submit():
task = Task(
title=form.title.data,
description=form.description.data,
user_id=current_user.id
)
db.session.add(task)
db.session.commit()
return redirect(url_for('tasks.home'))
return render_template('create_task.html', form=form)
Explanation:
login_required
- Restricts access to authenticated users.- Flask-WTF validates input for security.
Conclusion
Building a task management app with Flask, leveraging Werkzeug, Jinja2, and extensions like Flask-SQLAlchemy, Flask-Login, and Flask-WTF, enables developers to create a secure, user-friendly, and maintainable platform. Key takeaways:
- Use blueprints and application factories for modular design.
- Implement secure authentication and task authorization.
- Write tests to ensure reliability of core features.
- Avoid monolithic code and insecure practices like plain-text passwords.
With these practices, you can build a robust Flask task management app that is easy to maintain and extend!
Comments
Post a Comment