Flask Projects With Source Code

Expense Tracker App Project In Flask


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.

Written by Surfside Media

Senior Full Stack Developer specializing in Web Technologies.