diff --git a/README.md b/README.md index d55208e..89fc073 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # cal_counter Calorie Counter Webapp + +Bello -Iman diff --git a/app.py b/app.py new file mode 100644 index 0000000..77c5c4d --- /dev/null +++ b/app.py @@ -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) diff --git a/application/__init__.py b/application/__init__.py new file mode 100644 index 0000000..5c935b1 --- /dev/null +++ b/application/__init__.py @@ -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) diff --git a/application/admin/routes.py b/application/admin/routes.py new file mode 100644 index 0000000..ea4fb56 --- /dev/null +++ b/application/admin/routes.py @@ -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") diff --git a/application/admin/templates/food_items.html b/application/admin/templates/food_items.html new file mode 100644 index 0000000..9f7f311 --- /dev/null +++ b/application/admin/templates/food_items.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block content %} +Hallo +{% endblock%} \ No newline at end of file diff --git a/application/templates/base.html b/application/templates/base.html new file mode 100644 index 0000000..9a786f7 --- /dev/null +++ b/application/templates/base.html @@ -0,0 +1,58 @@ + + + + + + + {% block title %}My Flask App{% endblock %} + + + + + + + + + +
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
+ {% for message in messages %} +

{{ message }}

+ {% endfor %} +
+ {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + + + + \ No newline at end of file diff --git a/application/templates/dashboard.html b/application/templates/dashboard.html new file mode 100644 index 0000000..9f7f311 --- /dev/null +++ b/application/templates/dashboard.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block content %} +Hallo +{% endblock%} \ No newline at end of file diff --git a/application/templates/index.html b/application/templates/index.html new file mode 100644 index 0000000..74682ea --- /dev/null +++ b/application/templates/index.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block content %} +Index page idk +{% endblock%} \ No newline at end of file diff --git a/application/templates/login.html b/application/templates/login.html new file mode 100644 index 0000000..20fff43 --- /dev/null +++ b/application/templates/login.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} +
+ {{form.hidden_tag()}} + {{form.username()}} + {{form.password()}} + {{form.submit()}} +
+{% endblock%} \ No newline at end of file diff --git a/forms.py b/forms.py new file mode 100644 index 0000000..31abd81 --- /dev/null +++ b/forms.py @@ -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") diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -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 diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -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() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -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"} diff --git a/migrations/versions/319d293f3017_.py b/migrations/versions/319d293f3017_.py new file mode 100644 index 0000000..cde2f21 --- /dev/null +++ b/migrations/versions/319d293f3017_.py @@ -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 ### diff --git a/migrations/versions/aab8050e9c73_.py b/migrations/versions/aab8050e9c73_.py new file mode 100644 index 0000000..f636b43 --- /dev/null +++ b/migrations/versions/aab8050e9c73_.py @@ -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 ### diff --git a/models.py b/models.py new file mode 100644 index 0000000..47fba6b --- /dev/null +++ b/models.py @@ -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) diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..72063c2 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,6 @@ +{ + "exclude": [ + "migrations", + ".venv", + ] +} \ No newline at end of file diff --git a/seed.py b/seed.py new file mode 100644 index 0000000..963b42e --- /dev/null +++ b/seed.py @@ -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()