Creating an advanced-level Expense Tracker App using Flask and Bootstrap 5 involves designing a robust database structure, implementing Flask routes, and creating responsive and interactive web pages. Below is a detailed guide:
1. Database Structure
The database will store users, categories, expenses, and other related data. We'll use SQLAlchemy (an ORM for Flask) to manage the database.
Database Models
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
db = SQLAlchemy()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
email = db.Column(db.String(100), unique=True, nullable=False)
password = db.Column(db.String(200), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
expenses = db.relationship('Expense', backref='user', lazy=True)
categories = db.relationship('Category', backref='user', lazy=True)
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
expenses = db.relationship('Expense', backref='category', lazy=True)
class Expense(db.Model):
id = db.Column(db.Integer, primary_key=True)
amount = db.Column(db.Float, nullable=False)
description = db.Column(db.String(200))
date = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=False)
2. Flask Application Structure
Organize the project into the following structure:
expense_tracker/
│
├── app/
│ ├── __init__.py
│ ├── models.py
│ ├── routes/
│ │ ├── auth.py
│ │ ├── expenses.py
│ │ ├── categories.py
│ │ └── main.py
│ ├── templates/
│ │ ├── base.html
│ │ ├── index.html
│ │ ├── login.html
│ │ ├── register.html
│ │ ├── dashboard.html
│ │ ├── add_expense.html
│ │ ├── add_category.html
│ │ └── view_expenses.html
│ ├── static/
│ │ ├── css/
│ │ │ └── styles.css
│ │ └── js/
│ │ └── scripts.js
│ └── utils.py
│
├── config.py
├── requirements.txt
└── run.py
3. Flask Application Code
app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
db = SQLAlchemy()
login_manager = LoginManager()
def create_app():
app = Flask(__name__)
app.config.from_pyfile('config.py')
db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
from .routes.auth import auth_bp
from .routes.expenses import expenses_bp
from .routes.categories import categories_bp
from .routes.main import main_bp
app.register_blueprint(auth_bp)
app.register_blueprint(expenses_bp)
app.register_blueprint(categories_bp)
app.register_blueprint(main_bp)
return app
config.py
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'expense_tracker.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
run.py
from app import create_app, db
app = create_app()
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
4. Flask Routes
routes/auth.py
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required, current_user
from .forms import LoginForm, RegistrationForm
from ..models import User, db
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and user.verify_password(form.password.data):
login_user(user)
return redirect(url_for('main.dashboard'))
flash('Invalid email or password.')
return render_template('login.html', form=form)
@auth_bp.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('auth.login'))
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data, password=form.password.data)
db.session.add(user)
db.session.commit()
flash('Registration successful! You can now log in.')
return redirect(url_for('auth.login'))
return render_template('register.html', form=form)
routes/expenses.py
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_required, current_user
from ..models import Expense, db
from ..forms import ExpenseForm
expenses_bp = Blueprint('expenses', __name__)
@expenses_bp.route('/add_expense', methods=['GET', 'POST'])
@login_required
def add_expense():
form = ExpenseForm()
if form.validate_on_submit():
expense = Expense(amount=form.amount.data, description=form.description.data, user_id=current_user.id, category_id=form.category.data)
db.session.add(expense)
db.session.commit()
flash('Expense added successfully!')
return redirect(url_for('main.dashboard'))
return render_template('add_expense.html', form=form)
@expenses_bp.route('/view_expenses')
@login_required
def view_expenses():
expenses = Expense.query.filter_by(user_id=current_user.id).all()
return render_template('view_expenses.html', expenses=expenses)
routes/categories.py
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_required, current_user
from ..models import Category, db
from ..forms import CategoryForm
categories_bp = Blueprint('categories', __name__)
@categories_bp.route('/add_category', methods=['GET', 'POST'])
@login_required
def add_category():
form = CategoryForm()
if form.validate_on_submit():
category = Category(name=form.name.data, user_id=current_user.id)
db.session.add(category)
db.session.commit()
flash('Category added successfully!')
return redirect(url_for('main.dashboard'))
return render_template('add_category.html', form=form)
routes/main.py
from flask import Blueprint, render_template
from flask_login import login_required
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
def index():
return render_template('index.html')
@main_bp.route('/dashboard')
@login_required
def dashboard():
return render_template('dashboard.html')
5. HTML Templates with Bootstrap 5
templates/base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<title>{% block title %}Expense Tracker{% endblock %}</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('main.index') }}">Expense Tracker</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav me-auto">
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.dashboard') }}">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('expenses.view_expenses') }}">View Expenses</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('categories.add_category') }}">Add Category</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('expenses.add_expense') }}">Add Expense</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.logout') }}">Logout</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.register') }}">Register</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="alert alert-info">
{% for message in messages %}
<p>{{ message }}</p>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
templates/index.html
{% extends 'base.html' %}
{% block title %}Home{% endblock %}
{% block content %}
<h1>Welcome to the Expense Tracker App</h1>
<p>Track your expenses efficiently!</p>
{% endblock %}
templates/login.html
{% extends 'base.html' %}
{% block title %}Login{% endblock %}
{% block content %}
<h2>Login</h2>
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
{% endblock %}
templates/register.html
{% extends 'base.html' %}
{% block title %}Register{% endblock %}
{% block content %}
<h2>Register</h2>
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control") }}
</div>
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
</div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
{% endblock %}
templates/dashboard.html
{% extends 'base.html' %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h2>Dashboard</h2>
<p>Welcome, {{ current_user.username }}!</p>
{% endblock %}
templates/add_expense.html
{% extends 'base.html' %}
{% block title %}Add Expense{% endblock %}
{% block content %}
<h2>Add Expense</h2>
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.amount.label(class="form-label") }}
{{ form.amount(class="form-control") }}
</div>
<div class="mb-3">
{{ form.description.label(class="form-label") }}
{{ form.description(class="form-control") }}
</div>
<div class="mb-3">
{{ form.category.label(class="form-label") }}
{{ form.category(class="form-control") }}
</div>
<button type="submit" class="btn btn-primary">Add Expense</button>
</form>
{% endblock %}
templates/view_expenses.html
{% extends 'base.html' %}
{% block title %}View Expenses{% endblock %}
{% block content %}
<h2>Your Expenses</h2>
<table class="table">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Description</th>
<th scope="col">Amount</th>
<th scope="col">Category</th>
</tr>
</thead>
<tbody>
{% for expense in expenses %}
<tr>
<td>{{ expense.date.strftime('%Y-%m-%d') }}</td>
<td>{{ expense.description }}</td>
<td>${{ expense.amount }}</td>
<td>{{ expense.category.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
templates/add_category.html
{% extends 'base.html' %}
{% block title %}Add Category{% endblock %}
{% block content %}
<h2>Add Category</h2>
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.name.label(class="form-label") }}
{{ form.name(class="form-control") }}
</div>
<button type="submit" class="btn btn-primary">Add Category</button>
</form>
{% endblock %}
6. Forms for Flask-WTF
forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, FloatField, TextAreaField, SelectField, SubmitField
from wtforms.validators import DataRequired, Length, Email, EqualTo
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Login')
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=2, max=50)])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')
class ExpenseForm(FlaskForm):
amount = FloatField('Amount', validators=[DataRequired()])
description = TextAreaField('Description', validators=[DataRequired()])
category = SelectField('Category', coerce=int)
submit = SubmitField('Add Expense')
class CategoryForm(FlaskForm):
name = StringField('Category Name', validators=[DataRequired()])
submit = SubmitField('Add Category')
7. Static Files
static/css/styles.css
body {
background-color: #f8f9fa;
}
.navbar {
margin-bottom: 20px;
}
.table {
margin-top: 20px;
}
8. JavaScript (if needed)
static/js/scripts.js
// Custom JavaScript can be added here
9. Running the Application
To run the application, ensure you have the required packages installed as specified in requirements.txt, then execute:
python run.py
This structure and code provide a comprehensive foundation for an advanced Expense Tracker App using Flask and Bootstrap 5, allowing for user authentication, expense management, and category organization.