Initial commit: Flask calorie counter app setup

Add base Flask application with user authentication, SQLAlchemy models for users, units, and food items, admin blueprint, and basic templates. Includes database migration setup, login form, and seed script for initial user creation.
This commit is contained in:
2025-06-26 14:19:09 +02:00
parent b9f2c420ef
commit b498f3693a
19 changed files with 531 additions and 0 deletions

View File

@@ -1,2 +1,4 @@
# cal_counter # cal_counter
Calorie Counter Webapp Calorie Counter Webapp
Bello -Iman

65
app.py Normal file
View File

@@ -0,0 +1,65 @@
from flask import render_template, redirect, url_for
from flask_login import (
login_required,
logout_user,
login_user,
current_user,
)
from forms import LoginForm
from models import User
from application import db, app, login_manager
# Config
app.config["SECRET_KEY"] = "Iman"
login_manager.login_view = "login" # type: ignore
@login_manager.user_loader # type: ignore
def load_user(user_id: int):
return db.session.get(User, user_id)
# Routes
@app.route("/")
def index():
return render_template("index.html")
@app.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
return redirect(url_for("dashboard"))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(password=form.password.data):
# User found and password correct
login_user(user)
return redirect(url_for("dashboard"))
else:
pass
# invalid user
return render_template("login.html", form=form)
@app.route("/dashboard")
@login_required
def dashboard():
return render_template("dashboard.html", name=current_user.username)
@app.route("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for("index"))
# Run
if __name__ == "__main__":
app.run(debug=True)

18
application/__init__.py Normal file
View File

@@ -0,0 +1,18 @@
from flask import Flask
from flask_login import LoginManager # type: ignore
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from application.admin.routes import admin_bp
app = Flask(__name__) # Init Flask app
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///data.db"
db = SQLAlchemy(app=app) # Init SQLAlchemy
migrate = Migrate(app=app, db=db) # Init Migration
login_manager = LoginManager(app=app) # Init login manager
# Register blueprints
app.register_blueprint(admin_bp)

View File

@@ -0,0 +1,13 @@
from flask import Blueprint, render_template
admin_bp = Blueprint(
"admin",
__name__,
url_prefix="/admin",
template_folder="templates",
)
@admin_bp.route("/food_items", methods=["GET"])
def food_items():
return render_template("food_items.html")

View File

@@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block content %}
Hallo
{% endblock%}

View File

@@ -0,0 +1,58 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Flask App{% endblock %}</title>
<!-- Bootstrap 5 CDN -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('index') }}">Iman was here</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">Logout</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('dashboard') }}">Dashboard</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container">
{% 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.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block content %}
Hallo
{% endblock%}

View File

@@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block content %}
Index page idk
{% endblock%}

View File

@@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
<form method="post">
{{form.hidden_tag()}}
{{form.username()}}
{{form.password()}}
{{form.submit()}}
</form>
{% endblock%}

9
forms.py Normal file
View File

@@ -0,0 +1,9 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired
class LoginForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()])
password = PasswordField("Password", validators=[DataRequired()])
submit = SubmitField("Login")

1
migrations/README Normal file
View File

@@ -0,0 +1 @@
Single-database configuration for Flask.

50
migrations/alembic.ini Normal file
View File

@@ -0,0 +1,50 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

113
migrations/env.py Normal file
View File

@@ -0,0 +1,113 @@
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
def get_engine():
try:
# this works with Flask-SQLAlchemy<3 and Alchemical
return current_app.extensions['migrate'].db.get_engine()
except (TypeError, AttributeError):
# this works with Flask-SQLAlchemy>=3
return current_app.extensions['migrate'].db.engine
def get_engine_url():
try:
return get_engine().url.render_as_string(hide_password=False).replace(
'%', '%%')
except AttributeError:
return str(get_engine().url).replace('%', '%%')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option('sqlalchemy.url', get_engine_url())
target_db = current_app.extensions['migrate'].db
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def get_metadata():
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
conf_args = current_app.extensions['migrate'].configure_args
if conf_args.get("process_revision_directives") is None:
conf_args["process_revision_directives"] = process_revision_directives
connectable = get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=get_metadata(),
**conf_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,56 @@
"""empty message
Revision ID: 319d293f3017
Revises: aab8050e9c73
Create Date: 2025-06-26 10:35:18.540813
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '319d293f3017'
down_revision = 'aab8050e9c73'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('unit',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('symbol', sa.String(length=10), nullable=False),
sa.Column('name', sa.String(length=50), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name'),
sa.UniqueConstraint('symbol')
)
op.create_table('food_item',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=150), nullable=False),
sa.Column('amount', sa.Integer(), nullable=False),
sa.Column('unit_id', sa.Integer(), nullable=False),
sa.Column('energy', sa.Float(), nullable=True),
sa.Column('protein', sa.Float(), nullable=True),
sa.Column('carbs', sa.Float(), nullable=True),
sa.Column('sugar', sa.Float(), nullable=True),
sa.Column('fats', sa.Float(), nullable=True),
sa.Column('saturated_fats', sa.Float(), nullable=True),
sa.ForeignKeyConstraint(['unit_id'], ['unit.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.drop_table('units')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('units',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.drop_table('food_item')
op.drop_table('unit')
# ### end Alembic commands ###

View File

@@ -0,0 +1,39 @@
"""empty message
Revision ID: aab8050e9c73
Revises:
Create Date: 2025-06-26 10:24:25.760737
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'aab8050e9c73'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('units',
sa.Column('id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=150), nullable=False),
sa.Column('password', sa.String(length=150), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
op.drop_table('units')
# ### end Alembic commands ###

45
models.py Normal file
View File

@@ -0,0 +1,45 @@
from flask_login import UserMixin # type: ignore
from werkzeug.security import generate_password_hash, check_password_hash
from application import db
class User(UserMixin, db.Model):
__tablename__ = "user"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(150), unique=True, nullable=False)
password = db.Column(db.String, nullable=False)
def __init__(self, username: str, password: str):
super().__init__()
self.username = username
self.password = generate_password_hash(password=password)
def check_password(self, password: str) -> bool:
return check_password_hash(pwhash=self.password, password=password)
def change_password(self, password: str) -> None:
self.password = generate_password_hash(password=password)
class Units(db.Model):
__tablename__ = "unit"
id = db.Column(db.Integer, primary_key=True)
symbol = db.Column(db.String(10), unique=True, nullable=False)
name = db.Column(db.String(50), unique=True, nullable=False)
class FoodItems(db.Model):
__tablename__ = "food_item"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(150), unique=True, nullable=False)
amount = db.Column(db.Integer, nullable=False)
unit_id = db.Column(db.Integer, db.ForeignKey("unit.id"), nullable=False)
unit = db.relationship("Units")
energy = db.Column(db.Float)
protein = db.Column(db.Float)
carbs = db.Column(db.Float)
sugar = db.Column(db.Float)
fats = db.Column(db.Float)
saturated_fats = db.Column(db.Float)

6
pyrightconfig.json Normal file
View File

@@ -0,0 +1,6 @@
{
"exclude": [
"migrations",
".venv",
]
}

7
seed.py Normal file
View File

@@ -0,0 +1,7 @@
from application import db, app
from models import User
with app.app_context():
User.query.delete()
db.session.add(User("admin", "admin"))
db.session.commit()