From d78f48710e91aedbab6f3c028ffc0a2cd8cea5ae Mon Sep 17 00:00:00 2001 From: Stef Date: Thu, 14 Aug 2025 06:08:17 +0200 Subject: [PATCH 1/5] Add meal v2 flow and user timezone support Introduces a new add_meal_v2 blueprint with barcode scanning, item search, and improved meal logging UI. Adds user timezone support: login form now captures timezone, User model and database schema updated, and timezone is set on login. Refactors templates and forms to support these changes, and removes the old login template. --- app.py | 2 + application/add_meal_v2/routes.py | 187 ++++++++++++++++++ .../add_meal_v2/templates/add_item.html | 49 +++++ .../add_meal_v2/templates/scan_barcode.html | 134 +++++++++++++ application/add_meal_v2/templates/step4.html | 23 +++ application/auth/routes.py | 12 +- application/auth/templates/login.html | 18 +- application/templates/base.html | 3 + application/templates/login.html | 36 ---- application/user/templates/daily_log2.html | 19 +- application/utils.py | 10 + forms.py | 7 + migrations/versions/9eb23abd5294_.py | 40 ++++ models.py | 6 + 14 files changed, 503 insertions(+), 43 deletions(-) create mode 100644 application/add_meal_v2/routes.py create mode 100644 application/add_meal_v2/templates/add_item.html create mode 100644 application/add_meal_v2/templates/scan_barcode.html create mode 100644 application/add_meal_v2/templates/step4.html delete mode 100644 application/templates/login.html create mode 100644 migrations/versions/9eb23abd5294_.py diff --git a/app.py b/app.py index b937434..8342d29 100644 --- a/app.py +++ b/app.py @@ -13,6 +13,7 @@ from application.admin.routes import admin_bp from application.user.routes import user_bp from application.add_meal.routes import bp as add_meal_bp from application.auth.routes import bp as auth_bp +from application.add_meal_v2.routes import bp as add_meal_v2_bp from typing import Optional # Config @@ -31,6 +32,7 @@ app.register_blueprint(admin_bp) app.register_blueprint(user_bp) app.register_blueprint(add_meal_bp) app.register_blueprint(auth_bp) +app.register_blueprint(add_meal_v2_bp) # Routes diff --git a/application/add_meal_v2/routes.py b/application/add_meal_v2/routes.py new file mode 100644 index 0000000..8f10cd9 --- /dev/null +++ b/application/add_meal_v2/routes.py @@ -0,0 +1,187 @@ +from flask import ( + Blueprint, + redirect, + url_for, + render_template, + session, + request, + jsonify, + abort, +) +from flask_login import current_user +from forms import FoodItemForm, FoodLogForm +from application import db +from models import FoodItem, FoodLog +from sqlalchemy import and_, or_ +from datetime import datetime, timedelta, timezone +from sqlalchemy.sql.elements import BinaryExpression +from typing import cast + +bp = Blueprint( + "add_meal_v2", + __name__, + url_prefix="/add_meal_v2", + template_folder="templates", +) + + +@bp.before_request +def login_required(): + if not current_user.is_authenticated: + return redirect(url_for("auth.login")) + + +@bp.route("/select_meal/", methods=["GET"]) +def step1(meal_type: int): + assert type(meal_type) is int + assert 0 <= meal_type <= 3 + session["meal_type"] = meal_type + return redirect(url_for("add_meal.step2")) + + +@bp.route("/get_barcode", methods=["GET"]) +def step2(): + return render_template("scan_barcode.html") + + +@bp.route("/step3/", methods=["GET"]) +def step3(input: str): + # check if meal_type cookie is set + if "meal_type" not in session: + return redirect("/") + + # Check if input is a barcode + if input.isdigit(): + item = current_user.food_items.filter_by(barcode=input).first() + + else: + item = current_user.food_items.filter_by(name=input).first() + + if item is None: + # Does not exist, add item + return redirect(url_for("add_meal.step3_alt1", input=input)) + + # Track item to add and continue to next step + session["item_id"] = item.id + return redirect(url_for("add_meal.step4")) + + +@bp.route("/step3_alt1/", methods=["GET"]) +def step3_alt1(input: str): + form = FoodItemForm() + + if input.isdigit(): + form.barcode.data = input + else: + form.name.data = input + return render_template("add_item.html", form=form) + + +@bp.route("/step3_alt1/", methods=["POST"]) +def step3_alt1_post(input: str): + form = FoodItemForm() + + if form.validate_on_submit(): + # Form has valid input + barcode = form.barcode.data + name = form.name.data + assert name + assert form.energy.data is not None + assert form.protein.data is not None + assert form.carbs.data is not None + assert form.fat.data is not None + + # Check if name or barcode already exists + name_filter = cast(BinaryExpression, FoodItem.name == name) + barcode_filter = cast(BinaryExpression, FoodItem.barcode == barcode) + filter_exp = or_(name_filter, barcode_filter) + item = current_user.food_items.filter(filter_exp).first() + + if item is None: + # Item does not exist, add to DB + barcode = ( + barcode if barcode else None + ) # Turn empty strings into None + db.session.add( + FoodItem( + name=name, + owner_id=current_user.id, + energy=form.energy.data, + protein=form.protein.data, + carbs=form.carbs.data, + fat=form.fat.data, + barcode=barcode, + saturated_fat=form.saturated_fat.data, + sugar=form.sugar.data, + ) + ) + db.session.commit() + print("[DEBUG] New FoodItem Added") + input = barcode if barcode else name # update input + else: + print(f"Item exists: {item.barcode} {item.name}") + + # Item added or already present, return to step 3. + return redirect(url_for("add_meal.step3", input=input)) + else: + print("[DEBUG] Form Invalid") + return redirect(url_for("add_meal.step3_alt1", input=input)) + + +@bp.route("/step4", methods=["GET", "POST"]) +def step4(): + if "item_id" not in session: + return redirect(url_for("add_meal.step2")) + form = FoodLogForm() + item = db.session.get(FoodItem, session["item_id"]) + + offset = session["offset"] + if offset is None or item is None: + abort(404) + + today = datetime.now(timezone.utc).date() + day = today + timedelta(days=offset) + + if form.validate_on_submit(): + assert form.amount.data + db.session.add( + FoodLog( + food_item_id=item.id, + user_id=current_user.id, + amount=form.amount.data, + part_of_day=session["meal_type"], + date_=day, + ) + ) + db.session.commit() + session.pop("meal_type") + session.pop("item_id") + return redirect(url_for("user.daily_log", offset=offset)) + + match session["meal_type"]: + case 0: + tod = "Breakfast" + case 1: + tod = "Lunch" + case 2: + tod = "Dinner" + case 3: + tod = "Snack" + case _: + tod = "Unknown" + return render_template("step4.html", tod=tod, item=item, form=form) + + +@bp.route("/query", methods=["GET"]) +def query(): + q = request.args.get("q", "").strip().lower() + if not q: + return jsonify([]) + + words = q.split() + filters = [ + FoodItem.name.ilike(f"%{word}%") for word in words # type: ignore + ] + + results = current_user.food_items.filter(and_(*filters)).all() + return jsonify([item.name for item in results]) diff --git a/application/add_meal_v2/templates/add_item.html b/application/add_meal_v2/templates/add_item.html new file mode 100644 index 0000000..f44b33d --- /dev/null +++ b/application/add_meal_v2/templates/add_item.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} + +{% block content %} +
+ {{ form.hidden_tag() }} + +
+ {{ form.barcode.label(class="form-label") }} + {{ form.barcode(class="form-control-plaintext", readonly=true) }} +
+ +
+ {{ form.name.label(class="form-label") }} + {{ form.name(class="form-control") }} +
+ +
+ {{ form.energy.label(class="form-label") }} + {{ form.energy(class="form-control") }} +
+ +
+ {{ form.fat.label(class="form-label") }} + {{ form.fat(class="form-control") }} +
+ +
+ {{ form.saturated_fat.label(class="form-label") }} + {{ form.saturated_fat(class="form-control") }} +
+ +
+ {{ form.carbs.label(class="form-label") }} + {{ form.carbs(class="form-control") }} +
+ +
+ {{ form.sugar.label(class="form-label") }} + {{ form.sugar(class="form-control") }} +
+ +
+ {{ form.protein.label(class="form-label") }} + {{ form.protein(class="form-control") }} +
+ + {{ form.submit(class="btn btn-primary") }} +
+{% endblock%} \ No newline at end of file diff --git a/application/add_meal_v2/templates/scan_barcode.html b/application/add_meal_v2/templates/scan_barcode.html new file mode 100644 index 0000000..1169b99 --- /dev/null +++ b/application/add_meal_v2/templates/scan_barcode.html @@ -0,0 +1,134 @@ +{% extends "base.html" %} + +{% block content %} +
+ +
+

Barcode Scanner

+

Use your camera to scan barcodes

+
+ + +
+ +
+ + +
+ + +
+ + +
+
+ +
+ + +
+ + +
    +
    +
    + + + +
    +
      +
    +
    +
    + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/application/add_meal_v2/templates/step4.html b/application/add_meal_v2/templates/step4.html new file mode 100644 index 0000000..85cd332 --- /dev/null +++ b/application/add_meal_v2/templates/step4.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block content %} +

    {{ tod }}

    +

    {{ item.name }}

    + +
    + {{ form.hidden_tag() }} + +
    +
    + {{item_id}} +
    +
    + +
    + {{ form.amount.label(class="form-label") }} + {{ form.amount(class="form-control") }} +
    + + {{ form.submit(class="btn btn-primary") }} +
    +{% endblock%} \ No newline at end of file diff --git a/application/auth/routes.py b/application/auth/routes.py index 6d6f4b2..b14c3f9 100644 --- a/application/auth/routes.py +++ b/application/auth/routes.py @@ -1,8 +1,8 @@ -from flask import Blueprint, request, render_template, redirect, url_for +from flask import Blueprint, render_template, redirect, url_for from flask_login import current_user, login_user, logout_user from forms import LoginForm, ChangePasswordForm from models import User -from application.utils import default_return +from application.utils import default_return, is_valid_timezone from application import db bp = Blueprint( @@ -19,12 +19,16 @@ def login(): form = LoginForm() if form.validate_on_submit(): + assert form.timezone.data 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 - next_page = request.args.get("next") # Get next page if given + tz = form.timezone.data + if is_valid_timezone(tz): + user.set_timezone(tz) + db.session.commit() login_user(user) # Log in the user - return default_return(next_page=next_page) + return default_return() else: pass # invalid user diff --git a/application/auth/templates/login.html b/application/auth/templates/login.html index 69597be..e33c8db 100644 --- a/application/auth/templates/login.html +++ b/application/auth/templates/login.html @@ -3,7 +3,10 @@ {% block content %}
    -

    Login

    +

    Login

    +

    + Your timezone will be saved to show times correctly. +

    {{ form.hidden_tag() }} @@ -27,10 +30,21 @@ {% endif %}
    + {{ form.timezone(id="timezone") }} +
    {{ form.submit(class="btn btn-primary btn-lg") }}
    -{% endblock%} \ No newline at end of file +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/application/templates/base.html b/application/templates/base.html index a6e2fd3..7d14a2d 100644 --- a/application/templates/base.html +++ b/application/templates/base.html @@ -83,6 +83,9 @@ + + {% block scripts %} + {% endblock %} \ No newline at end of file diff --git a/application/templates/login.html b/application/templates/login.html deleted file mode 100644 index e65d635..0000000 --- a/application/templates/login.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -
    -
    -

    Login

    -
    - {{ form.hidden_tag() }} - -
    - {{ form.username.label(class="form-label") }} - {{ form.username(class="form-control", placeholder="Enter username") }} - {% if form.username.errors %} -
    - {{ form.username.errors[0] }} -
    - {% endif %} -
    - -
    - {{ form.password.label(class="form-label") }} - {{ form.password(class="form-control", placeholder="Enter password") }} - {% if form.password.errors %} -
    - {{ form.password.errors[0] }} -
    - {% endif %} -
    - -
    - {{ form.submit(class="btn btn-primary btn-lg") }} -
    -
    -
    -
    -{% endblock%} \ No newline at end of file diff --git a/application/user/templates/daily_log2.html b/application/user/templates/daily_log2.html index 6e72d98..fed4194 100644 --- a/application/user/templates/daily_log2.html +++ b/application/user/templates/daily_log2.html @@ -52,7 +52,7 @@ - + Add Item @@ -64,4 +64,21 @@ +{% endblock %} + +{% block scripts %} + {% endblock %} \ No newline at end of file diff --git a/application/utils.py b/application/utils.py index 6631122..74f14cb 100644 --- a/application/utils.py +++ b/application/utils.py @@ -1,6 +1,7 @@ from flask_login import current_user from flask import redirect, url_for, flash from typing import Optional +from zoneinfo import ZoneInfo def login_required(): @@ -19,3 +20,12 @@ def default_return(next_page: Optional[str] = None): if current_user.is_admin: return redirect(url_for("admin.food_items")) return redirect(url_for("dashboard")) + + +def is_valid_timezone(tz: str) -> bool: + try: + ZoneInfo(tz) + except Exception: + print(Exception) + return False + return True diff --git a/forms.py b/forms.py index 0497b9e..ea3ab1d 100644 --- a/forms.py +++ b/forms.py @@ -4,13 +4,20 @@ from wtforms import ( PasswordField, SubmitField, FloatField, + HiddenField, ) from wtforms.validators import DataRequired, InputRequired, Optional +class SelectDateForm(FlaskForm): + date = HiddenField(validators=[DataRequired()]) + submit = SubmitField("+ Add Item") + + class LoginForm(FlaskForm): username = StringField("Username", validators=[DataRequired()]) password = PasswordField("Password", validators=[DataRequired()]) + timezone = HiddenField("Timezone", validators=[DataRequired()]) submit = SubmitField("Log in") diff --git a/migrations/versions/9eb23abd5294_.py b/migrations/versions/9eb23abd5294_.py new file mode 100644 index 0000000..34381ae --- /dev/null +++ b/migrations/versions/9eb23abd5294_.py @@ -0,0 +1,40 @@ +"""empty message + +Revision ID: 9eb23abd5294 +Revises: 101002a6ef17 +Create Date: 2025-08-14 05:40:27.342711 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "9eb23abd5294" +down_revision = "101002a6ef17" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("user", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "timezone", + sa.String(length=64), + nullable=False, + server_default="UTC", + ) + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("user", schema=None) as batch_op: + batch_op.drop_column("timezone") + + # ### end Alembic commands ### diff --git a/models.py b/models.py index 32d27d9..667b01d 100644 --- a/models.py +++ b/models.py @@ -4,6 +4,7 @@ from application import db from typing import Optional from forms import FoodItemForm from datetime import datetime, timezone, date +from application.utils import is_valid_timezone class User(UserMixin, db.Model): @@ -11,6 +12,7 @@ class User(UserMixin, db.Model): 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) + timezone = db.Column(db.String(64), nullable=False, default="UTC") is_admin = db.Column(db.Boolean, nullable=False, default=False) must_change_password = db.Column(db.Boolean, nullable=False, default=False) @@ -39,6 +41,10 @@ class User(UserMixin, db.Model): def set_pw_change(self, change: bool) -> None: self.must_change_password = change + def set_timezone(self, tz: str) -> None: + if is_valid_timezone(tz): + self.timezone = tz + class Unit(db.Model): __tablename__ = "unit" From 4f1b5a56677537f527d99958de1fb17e3e1215ff Mon Sep 17 00:00:00 2001 From: Stef Date: Thu, 14 Aug 2025 06:59:29 +0200 Subject: [PATCH 2/5] Refactor meal addition flow and improve date handling Refactored add_meal_v2 routes to simplify and clarify the meal addition process, including renaming endpoints and templates, and introducing decorators for date and item selection. Updated daily_log2 to use user's timezone and display the selected date. Adjusted templates and barcode scan logic to match new routes and improved user experience. --- application/add_meal_v2/routes.py | 93 +++++++++---------- ...scan_barcode.html => scan_barcode_v2.html} | 4 +- application/user/routes.py | 28 ++++-- application/user/templates/daily_log2.html | 21 +---- 4 files changed, 68 insertions(+), 78 deletions(-) rename application/add_meal_v2/templates/{scan_barcode.html => scan_barcode_v2.html} (96%) diff --git a/application/add_meal_v2/routes.py b/application/add_meal_v2/routes.py index 8f10cd9..f11dbec 100644 --- a/application/add_meal_v2/routes.py +++ b/application/add_meal_v2/routes.py @@ -6,14 +6,13 @@ from flask import ( session, request, jsonify, - abort, ) from flask_login import current_user from forms import FoodItemForm, FoodLogForm from application import db from models import FoodItem, FoodLog from sqlalchemy import and_, or_ -from datetime import datetime, timedelta, timezone +from datetime import datetime from sqlalchemy.sql.elements import BinaryExpression from typing import cast @@ -25,31 +24,34 @@ bp = Blueprint( ) +def date_present(func): + def check_date(): + if "selected_date" not in session: + return redirect(url_for("user.daily_log2")) + + +def item_selected(func): + def check_item(): + if check_item(): + if "item_id" not in session: + return redirect(url_for("add_meal_v2.get_barcode")) + + @bp.before_request def login_required(): if not current_user.is_authenticated: return redirect(url_for("auth.login")) -@bp.route("/select_meal/", methods=["GET"]) -def step1(meal_type: int): - assert type(meal_type) is int - assert 0 <= meal_type <= 3 - session["meal_type"] = meal_type - return redirect(url_for("add_meal.step2")) - - +@date_present @bp.route("/get_barcode", methods=["GET"]) -def step2(): - return render_template("scan_barcode.html") +def get_barcode(): + return render_template("scan_barcode_v2.html") -@bp.route("/step3/", methods=["GET"]) -def step3(input: str): - # check if meal_type cookie is set - if "meal_type" not in session: - return redirect("/") - +@date_present +@bp.route("/add_existing/", methods=["GET"]) +def add_existing(input: str): # Check if input is a barcode if input.isdigit(): item = current_user.food_items.filter_by(barcode=input).first() @@ -59,15 +61,16 @@ def step3(input: str): if item is None: # Does not exist, add item - return redirect(url_for("add_meal.step3_alt1", input=input)) + return redirect(url_for("add_meal_v2.add_new", input=input)) # Track item to add and continue to next step session["item_id"] = item.id - return redirect(url_for("add_meal.step4")) + return redirect(url_for("add_meal_v2.step4")) -@bp.route("/step3_alt1/", methods=["GET"]) -def step3_alt1(input: str): +@date_present +@bp.route("/add_new/", methods=["GET"]) +def add_new(input: str): form = FoodItemForm() if input.isdigit(): @@ -77,8 +80,9 @@ def step3_alt1(input: str): return render_template("add_item.html", form=form) -@bp.route("/step3_alt1/", methods=["POST"]) -def step3_alt1_post(input: str): +@date_present +@bp.route("/add_new/", methods=["POST"]) +def add_new_post(input: str): form = FoodItemForm() if form.validate_on_submit(): @@ -122,25 +126,20 @@ def step3_alt1_post(input: str): print(f"Item exists: {item.barcode} {item.name}") # Item added or already present, return to step 3. - return redirect(url_for("add_meal.step3", input=input)) + return redirect(url_for("add_meal_v2.step3", input=input)) else: print("[DEBUG] Form Invalid") - return redirect(url_for("add_meal.step3_alt1", input=input)) + return redirect(url_for("add_meal_v2.step3_alt1", input=input)) +@date_present +@item_selected @bp.route("/step4", methods=["GET", "POST"]) def step4(): - if "item_id" not in session: - return redirect(url_for("add_meal.step2")) form = FoodLogForm() item = db.session.get(FoodItem, session["item_id"]) - - offset = session["offset"] - if offset is None or item is None: - abort(404) - - today = datetime.now(timezone.utc).date() - day = today + timedelta(days=offset) + if not item: + return redirect(url_for("add_meal_v2.get_barcode")) if form.validate_on_submit(): assert form.amount.data @@ -149,29 +148,21 @@ def step4(): food_item_id=item.id, user_id=current_user.id, amount=form.amount.data, - part_of_day=session["meal_type"], - date_=day, + part_of_day=0, + date_=datetime.strptime( + session["selected_date"], "%Y-%m-%d" + ).date(), ) ) db.session.commit() - session.pop("meal_type") session.pop("item_id") - return redirect(url_for("user.daily_log", offset=offset)) + session.pop("selected_date") + return redirect(url_for("user.daily_log2")) - match session["meal_type"]: - case 0: - tod = "Breakfast" - case 1: - tod = "Lunch" - case 2: - tod = "Dinner" - case 3: - tod = "Snack" - case _: - tod = "Unknown" - return render_template("step4.html", tod=tod, item=item, form=form) + return render_template("step4.html", tod="idk", item=item, form=form) +@date_present @bp.route("/query", methods=["GET"]) def query(): q = request.args.get("q", "").strip().lower() diff --git a/application/add_meal_v2/templates/scan_barcode.html b/application/add_meal_v2/templates/scan_barcode_v2.html similarity index 96% rename from application/add_meal_v2/templates/scan_barcode.html rename to application/add_meal_v2/templates/scan_barcode_v2.html index 1169b99..dd6655d 100644 --- a/application/add_meal_v2/templates/scan_barcode.html +++ b/application/add_meal_v2/templates/scan_barcode_v2.html @@ -85,7 +85,7 @@ goButton.addEventListener('click', () => { const value = searchBox.value.trim(); if (value) { - const baseURL = "{{url_for('add_meal.step3', input='!')}}"; + const baseURL = "{{url_for('add_meal_v2.add_existing', input='!')}}"; window.location.href = baseURL.replace("!", encodeURIComponent(value)); } }); @@ -121,7 +121,7 @@ if (result) { // Result found, this should post the barcode const codeText = result.getText(); - const baseURL = "{{url_for('add_meal.step3', input='!')}}"; + const baseURL = "{{url_for('add_meal_v2.add_existing', input='!')}}"; window.location.href = baseURL.replace("!", encodeURIComponent(codeText)); } }) diff --git a/application/user/routes.py b/application/user/routes.py index 395f892..9b61b52 100644 --- a/application/user/routes.py +++ b/application/user/routes.py @@ -13,8 +13,8 @@ from forms import FoodItemForm from models import FoodItem, FoodLog from datetime import datetime, timezone, timedelta from application.utils import login_required -from typing import cast from numpy import array +from zoneinfo import ZoneInfo user_bp = Blueprint( "user", @@ -155,14 +155,30 @@ def daily_log(offset: int = 0): @user_bp.route("/daily_log2", methods=["GET"]) def daily_log2(): - today = datetime.now(timezone.utc).date() - logs_today = current_user.food_logs.filter_by(date_=today).all() + # Get today's date according to user's timezone + today = datetime.now(ZoneInfo(current_user.timezone)).date() + + # Save date in session + session["selected_date"] = today.isoformat() + + # Get logs from today + logs_today = current_user.food_logs.filter_by( + date_=today.isoformat() + ).all() + + # calculate macros macros = array((0.0, 0.0, 0.0, 0.0)) for log in logs_today: - item = cast(FoodItem, log.food_item) - macros += array(item.macros()) / 100 * log.amount + macros += array(log.food_item.macros()) / 100 * log.amount macros = macro_arr_to_json(macros.tolist()) - return render_template("daily_log2.html", macros=macros, logs=logs_today) + + # Render HTML + return render_template( + "daily_log2.html", + macros=macros, + logs=logs_today, + today=today.strftime("%d/%m/%Y"), + ) @user_bp.route("/remove_log/", methods=["POST"]) diff --git a/application/user/templates/daily_log2.html b/application/user/templates/daily_log2.html index fed4194..e46c32e 100644 --- a/application/user/templates/daily_log2.html +++ b/application/user/templates/daily_log2.html @@ -6,7 +6,7 @@
    -

    Daily Calorie Dashboard

    +

    Daily Calorie Dashboard ({{ today }})

    @@ -52,7 +52,7 @@ - + Add Item @@ -64,21 +64,4 @@
    -{% endblock %} - -{% block scripts %} - {% endblock %} \ No newline at end of file From f7f6d235625f6104b4e3f4f0002a899d82ddc98f Mon Sep 17 00:00:00 2001 From: Stef Date: Thu, 14 Aug 2025 12:29:02 +0200 Subject: [PATCH 3/5] Removed old log for new log --- app.py | 4 +- application/add_meal/routes.py | 187 ---------------- application/add_meal/templates/add_item.html | 49 ---- .../add_meal/templates/scan_barcode.html | 134 ----------- application/add_meal/templates/step4.html | 23 -- application/add_meal_v2/routes.py | 4 +- application/user/routes.py | 5 +- application/user/templates/daily_log.html | 211 ------------------ application/user/templates/daily_log2.html | 8 +- application/utils.py | 2 +- migrations/versions/65eaeafb0904_.py | 32 +++ models.py | 1 - 12 files changed, 42 insertions(+), 618 deletions(-) delete mode 100644 application/add_meal/routes.py delete mode 100644 application/add_meal/templates/add_item.html delete mode 100644 application/add_meal/templates/scan_barcode.html delete mode 100644 application/add_meal/templates/step4.html delete mode 100644 application/user/templates/daily_log.html create mode 100644 migrations/versions/65eaeafb0904_.py diff --git a/app.py b/app.py index 8342d29..6fcdea0 100644 --- a/app.py +++ b/app.py @@ -11,7 +11,6 @@ from models import User from application import db, app, login_manager from application.admin.routes import admin_bp from application.user.routes import user_bp -from application.add_meal.routes import bp as add_meal_bp from application.auth.routes import bp as auth_bp from application.add_meal_v2.routes import bp as add_meal_v2_bp from typing import Optional @@ -30,14 +29,13 @@ def load_user(user_id: int): # Register blueprints app.register_blueprint(admin_bp) app.register_blueprint(user_bp) -app.register_blueprint(add_meal_bp) app.register_blueprint(auth_bp) app.register_blueprint(add_meal_v2_bp) # Routes def default_return(next_page: Optional[str] = None): - return redirect(url_for("user.daily_log")) + return redirect(url_for("user.daily_log2")) if next_page: return redirect(next_page) if current_user.is_admin: diff --git a/application/add_meal/routes.py b/application/add_meal/routes.py deleted file mode 100644 index 7efdf0e..0000000 --- a/application/add_meal/routes.py +++ /dev/null @@ -1,187 +0,0 @@ -from flask import ( - Blueprint, - redirect, - url_for, - render_template, - session, - request, - jsonify, - abort, -) -from flask_login import current_user -from forms import FoodItemForm, FoodLogForm -from application import db -from models import FoodItem, FoodLog -from sqlalchemy import and_, or_ -from datetime import datetime, timedelta, timezone -from sqlalchemy.sql.elements import BinaryExpression -from typing import cast - -bp = Blueprint( - "add_meal", - __name__, - url_prefix="/add_meal", - template_folder="templates", -) - - -@bp.before_request -def login_required(): - if not current_user.is_authenticated: - return redirect(url_for("auth.login")) - - -@bp.route("/select_meal/", methods=["GET"]) -def step1(meal_type: int): - assert type(meal_type) is int - assert 0 <= meal_type <= 3 - session["meal_type"] = meal_type - return redirect(url_for("add_meal.step2")) - - -@bp.route("/get_barcode", methods=["GET"]) -def step2(): - return render_template("scan_barcode.html") - - -@bp.route("/step3/", methods=["GET"]) -def step3(input: str): - # check if meal_type cookie is set - if "meal_type" not in session: - return redirect("/") - - # Check if input is a barcode - if input.isdigit(): - item = current_user.food_items.filter_by(barcode=input).first() - - else: - item = current_user.food_items.filter_by(name=input).first() - - if item is None: - # Does not exist, add item - return redirect(url_for("add_meal.step3_alt1", input=input)) - - # Track item to add and continue to next step - session["item_id"] = item.id - return redirect(url_for("add_meal.step4")) - - -@bp.route("/step3_alt1/", methods=["GET"]) -def step3_alt1(input: str): - form = FoodItemForm() - - if input.isdigit(): - form.barcode.data = input - else: - form.name.data = input - return render_template("add_item.html", form=form) - - -@bp.route("/step3_alt1/", methods=["POST"]) -def step3_alt1_post(input: str): - form = FoodItemForm() - - if form.validate_on_submit(): - # Form has valid input - barcode = form.barcode.data - name = form.name.data - assert name - assert form.energy.data is not None - assert form.protein.data is not None - assert form.carbs.data is not None - assert form.fat.data is not None - - # Check if name or barcode already exists - name_filter = cast(BinaryExpression, FoodItem.name == name) - barcode_filter = cast(BinaryExpression, FoodItem.barcode == barcode) - filter_exp = or_(name_filter, barcode_filter) - item = current_user.food_items.filter(filter_exp).first() - - if item is None: - # Item does not exist, add to DB - barcode = ( - barcode if barcode else None - ) # Turn empty strings into None - db.session.add( - FoodItem( - name=name, - owner_id=current_user.id, - energy=form.energy.data, - protein=form.protein.data, - carbs=form.carbs.data, - fat=form.fat.data, - barcode=barcode, - saturated_fat=form.saturated_fat.data, - sugar=form.sugar.data, - ) - ) - db.session.commit() - print("[DEBUG] New FoodItem Added") - input = barcode if barcode else name # update input - else: - print(f"Item exists: {item.barcode} {item.name}") - - # Item added or already present, return to step 3. - return redirect(url_for("add_meal.step3", input=input)) - else: - print("[DEBUG] Form Invalid") - return redirect(url_for("add_meal.step3_alt1", input=input)) - - -@bp.route("/step4", methods=["GET", "POST"]) -def step4(): - if "item_id" not in session: - return redirect(url_for("add_meal.step2")) - form = FoodLogForm() - item = db.session.get(FoodItem, session["item_id"]) - - offset = session["offset"] - if offset is None or item is None: - abort(404) - - today = datetime.now(timezone.utc).date() - day = today + timedelta(days=offset) - - if form.validate_on_submit(): - assert form.amount.data - db.session.add( - FoodLog( - food_item_id=item.id, - user_id=current_user.id, - amount=form.amount.data, - part_of_day=session["meal_type"], - date_=day, - ) - ) - db.session.commit() - session.pop("meal_type") - session.pop("item_id") - return redirect(url_for("user.daily_log", offset=offset)) - - match session["meal_type"]: - case 0: - tod = "Breakfast" - case 1: - tod = "Lunch" - case 2: - tod = "Dinner" - case 3: - tod = "Snack" - case _: - tod = "Unknown" - return render_template("step4.html", tod=tod, item=item, form=form) - - -@bp.route("/query", methods=["GET"]) -def query(): - q = request.args.get("q", "").strip().lower() - if not q: - return jsonify([]) - - words = q.split() - filters = [ - FoodItem.name.ilike(f"%{word}%") for word in words # type: ignore - ] - - results = current_user.food_items.filter(and_(*filters)).all() - return jsonify([item.name for item in results]) diff --git a/application/add_meal/templates/add_item.html b/application/add_meal/templates/add_item.html deleted file mode 100644 index f44b33d..0000000 --- a/application/add_meal/templates/add_item.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -
    - {{ form.hidden_tag() }} - -
    - {{ form.barcode.label(class="form-label") }} - {{ form.barcode(class="form-control-plaintext", readonly=true) }} -
    - -
    - {{ form.name.label(class="form-label") }} - {{ form.name(class="form-control") }} -
    - -
    - {{ form.energy.label(class="form-label") }} - {{ form.energy(class="form-control") }} -
    - -
    - {{ form.fat.label(class="form-label") }} - {{ form.fat(class="form-control") }} -
    - -
    - {{ form.saturated_fat.label(class="form-label") }} - {{ form.saturated_fat(class="form-control") }} -
    - -
    - {{ form.carbs.label(class="form-label") }} - {{ form.carbs(class="form-control") }} -
    - -
    - {{ form.sugar.label(class="form-label") }} - {{ form.sugar(class="form-control") }} -
    - -
    - {{ form.protein.label(class="form-label") }} - {{ form.protein(class="form-control") }} -
    - - {{ form.submit(class="btn btn-primary") }} -
    -{% endblock%} \ No newline at end of file diff --git a/application/add_meal/templates/scan_barcode.html b/application/add_meal/templates/scan_barcode.html deleted file mode 100644 index 1169b99..0000000 --- a/application/add_meal/templates/scan_barcode.html +++ /dev/null @@ -1,134 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -
    - -
    -

    Barcode Scanner

    -

    Use your camera to scan barcodes

    -
    - - -
    - -
    - - -
    - - -
    - - -
    -
    - -
    - - -
    - - -
      -
      -
      - - - -
      -
        -
      -
      -
      - - - - - - - - -{% endblock %} \ No newline at end of file diff --git a/application/add_meal/templates/step4.html b/application/add_meal/templates/step4.html deleted file mode 100644 index 85cd332..0000000 --- a/application/add_meal/templates/step4.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

      {{ tod }}

      -

      {{ item.name }}

      - -
      - {{ form.hidden_tag() }} - -
      -
      - {{item_id}} -
      -
      - -
      - {{ form.amount.label(class="form-label") }} - {{ form.amount(class="form-control") }} -
      - - {{ form.submit(class="btn btn-primary") }} -
      -{% endblock%} \ No newline at end of file diff --git a/application/add_meal_v2/routes.py b/application/add_meal_v2/routes.py index f11dbec..1aef4a4 100644 --- a/application/add_meal_v2/routes.py +++ b/application/add_meal_v2/routes.py @@ -126,10 +126,10 @@ def add_new_post(input: str): print(f"Item exists: {item.barcode} {item.name}") # Item added or already present, return to step 3. - return redirect(url_for("add_meal_v2.step3", input=input)) + return redirect(url_for("add_meal_v2.add_existing", input=input)) else: print("[DEBUG] Form Invalid") - return redirect(url_for("add_meal_v2.step3_alt1", input=input)) + return redirect(url_for("add_meal_v2.add_new", input=input)) @date_present diff --git a/application/user/routes.py b/application/user/routes.py index 9b61b52..d0380a6 100644 --- a/application/user/routes.py +++ b/application/user/routes.py @@ -178,6 +178,7 @@ def daily_log2(): macros=macros, logs=logs_today, today=today.strftime("%d/%m/%Y"), + min=min, ) @@ -191,6 +192,4 @@ def remove_log(id: int): # Delete log db.session.delete(log) db.session.commit() - if "offset" in session: - return redirect(url_for("user.daily_log", offset=session["offset"])) - return redirect(url_for("user.daily_log")) + return redirect(url_for("user.daily_log2")) diff --git a/application/user/templates/daily_log.html b/application/user/templates/daily_log.html deleted file mode 100644 index 0c57753..0000000 --- a/application/user/templates/daily_log.html +++ /dev/null @@ -1,211 +0,0 @@ -{% extends "base.html" %} - -{% block title %} -Food Nutritional Info -{% endblock %} - -{% block content %} - - -
      - -
      - -
      - -
      - - -
      -

      Daily Overview ({{date}})

      - - -
      -
      - Calories: {{ '%.0f' % calories }} kcal -
      -
      - Protein: {{ '%.1f' % protein }} g -
      -
      - - -
      -
      - Carbs: {{ '%.1f' % carbs }} g -
      -
      - Fat: {{ '%.1f' % fat }} g -
      -
      -
      - - -
      - -
      -
      - - - - -
      -
      -

      Eaten today

      -
      -
      - -
      -
      -

      Breakfast

      -
      -
      -

      Amount

      -
      -
      - Add -
      -
      - - -
      - {% for log in logs[0] %} -
      -
      - {{ log.food_item.name }} -
      -
      - {{ "{:g}".format(log.amount) }} -
      -
      -
      - -
      -
      -
      - {% endfor %} -
      -
      - - - - -
      - -
      -
      -

      Lunch

      -
      -
      -

      Amount

      -
      -
      - Add -
      -
      - - -
      - {% for log in logs[1] %} -
      -
      - {{ log.food_item.name }} -
      -
      - {{ "{:g}".format(log.amount) }} -
      -
      -
      - -
      -
      -
      - {% endfor %} -
      -
      - -
      - -
      -
      -

      Dinner

      -
      -
      -

      Amount

      -
      -
      - Add -
      -
      - - -
      - {% for log in logs[2] %} -
      -
      - {{ log.food_item.name }} -
      -
      - {{ "{:g}".format(log.amount) }} -
      -
      -
      - -
      -
      -
      - {% endfor %} -
      -
      - -
      - -
      -
      -

      Snacks

      -
      -
      -

      Amount

      -
      -
      - Add -
      -
      - - -
      - {% for log in logs[3] %} -
      -
      - {{ log.food_item.name }} -
      -
      - {{ "{:g}".format(log.amount) }} -
      -
      -
      - -
      -
      -
      - {% endfor %} -
      -
      -
      - - {% endblock%} \ No newline at end of file diff --git a/application/user/templates/daily_log2.html b/application/user/templates/daily_log2.html index e46c32e..c3a7e51 100644 --- a/application/user/templates/daily_log2.html +++ b/application/user/templates/daily_log2.html @@ -17,10 +17,10 @@
      - {{ macro.current - macro.target }}{{ macro.unit }} + {{ (macro.current - macro.target) }}{{ macro.unit }}
      - {{ macro.current }}{{ macro.unit }} + {{ min(macro.current, macro.target) }}{{ macro.unit }}
      @@ -34,7 +34,7 @@ {% for log in logs %}
      ({{ log.amount }}g) {{ log.food_item.name }} - {{ log.food_item.energy_100 }} kcal + {{ log.food_item.energy_100 * log.amount / 100 }} kcal
      {% endfor %} @@ -52,7 +52,7 @@ - + Add Item diff --git a/application/utils.py b/application/utils.py index 74f14cb..fac23ae 100644 --- a/application/utils.py +++ b/application/utils.py @@ -14,7 +14,7 @@ def login_required(): def default_return(next_page: Optional[str] = None): - return redirect(url_for("user.daily_log")) + return redirect(url_for("user.daily_log2")) if next_page: return redirect(next_page) if current_user.is_admin: diff --git a/migrations/versions/65eaeafb0904_.py b/migrations/versions/65eaeafb0904_.py new file mode 100644 index 0000000..bb66806 --- /dev/null +++ b/migrations/versions/65eaeafb0904_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 65eaeafb0904 +Revises: 9eb23abd5294 +Create Date: 2025-08-14 12:28:56.157288 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '65eaeafb0904' +down_revision = '9eb23abd5294' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('food_log', schema=None) as batch_op: + batch_op.drop_column('part_of_day') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('food_log', schema=None) as batch_op: + batch_op.add_column(sa.Column('part_of_day', sa.INTEGER(), nullable=False)) + + # ### end Alembic commands ### diff --git a/models.py b/models.py index 667b01d..7c6b27e 100644 --- a/models.py +++ b/models.py @@ -134,7 +134,6 @@ class FoodLog(db.Model): food_item_id = db.Column( db.Integer, db.ForeignKey("food_item.id"), nullable=False ) - part_of_day = db.Column(db.Integer, nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) amount = db.Column(db.Float, nullable=False) From c3e9c8631d6b8898cd4ad3e87a84c3b3289b4965 Mon Sep 17 00:00:00 2001 From: Stef Date: Thu, 14 Aug 2025 12:32:00 +0200 Subject: [PATCH 4/5] Remove part of day from remaining code --- application/add_meal_v2/routes.py | 1 - application/templates/base.html | 3 --- application/user/routes.py | 36 +------------------------------ models.py | 2 -- seed.py | 10 ++++----- 5 files changed, 6 insertions(+), 46 deletions(-) diff --git a/application/add_meal_v2/routes.py b/application/add_meal_v2/routes.py index 1aef4a4..fbe3aa2 100644 --- a/application/add_meal_v2/routes.py +++ b/application/add_meal_v2/routes.py @@ -148,7 +148,6 @@ def step4(): food_item_id=item.id, user_id=current_user.id, amount=form.amount.data, - part_of_day=0, date_=datetime.strptime( session["selected_date"], "%Y-%m-%d" ).date(), diff --git a/application/templates/base.html b/application/templates/base.html index 7d14a2d..70ef8ad 100644 --- a/application/templates/base.html +++ b/application/templates/base.html @@ -22,9 +22,6 @@