diff --git a/application/add_meal/routes.py b/application/add_meal/routes.py index 8df37e7..ada458d 100644 --- a/application/add_meal/routes.py +++ b/application/add_meal/routes.py @@ -12,8 +12,10 @@ from flask_login import current_user from forms import FoodItemForm, FoodLogForm from application import db from models import FoodItem, FoodLog -from sqlalchemy import and_ +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", @@ -51,59 +53,23 @@ def step3(input: str): # Check if input is a barcode if input.isdigit(): item = current_user.food_items.filter_by(barcode=input).first() - if item is None: - # Does not exist, add item - return redirect(url_for("add_meal.step3_alt1", input=input)) + else: - # input is not a number, must be the name of the item. item = current_user.food_items.filter_by(name=input).first() - if item is None: - # Does not exist, add manually. - return redirect(url_for("add_meal.step3_alt1", input=input)) + + 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", "POST"]) +@bp.route("/step3_alt1/", methods=["GET"]) def step3_alt1(input: str): form = FoodItemForm() - if form.validate_on_submit(): - print("[DEBUG] Valid form") - if ( - current_user.food_items.filter_by( - barcode=form.barcode.data - ).first() - is None - ): - assert form.name.data is not None - 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 - assert form.barcode.data is not None - db.session.add( - FoodItem( - name=form.name.data, - owner_id=current_user.id, - energy=form.energy.data, - protein=form.protein.data, - carbs=form.carbs.data, - fat=form.fat.data, - barcode=( - form.barcode.data - if form.barcode.data.isdigit() - else None - ), - saturated_fat=form.saturated_fat.data, - sugar=form.sugar.data, - ) - ) - db.session.commit() - print("[DEBUG] New item added") - return redirect(url_for("add_meal.step3", input=form.barcode.data)) - print("[DEBUG] Invalid form") + if input.isdigit(): form.barcode.data = input else: @@ -111,6 +77,56 @@ 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): + 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") + 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: diff --git a/forms.py b/forms.py index f5781eb..d4d373b 100644 --- a/forms.py +++ b/forms.py @@ -16,7 +16,7 @@ class LoginForm(FlaskForm): class FoodItemForm(FlaskForm): - barcode = StringField("Barcode", validators=[InputRequired()]) + barcode = StringField("Barcode", validators=[Optional()]) name = StringField("Product Name", validators=[DataRequired()]) energy = IntegerField("Energy per 100g", validators=[InputRequired()]) protein = FloatField("protein per 100g", validators=[InputRequired()]) diff --git a/migrations/versions/99f86450e4af_.py b/migrations/versions/99f86450e4af_.py new file mode 100644 index 0000000..f3a878b --- /dev/null +++ b/migrations/versions/99f86450e4af_.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: 99f86450e4af +Revises: bb1d9bebf8f6 +Create Date: 2025-08-11 12:36:26.924696 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '99f86450e4af' +down_revision = 'bb1d9bebf8f6' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('food_item', schema=None) as batch_op: + batch_op.drop_constraint(batch_op.f('barcode_owner_key'), type_='unique') + batch_op.create_unique_constraint('name_owner_key', ['name', 'owner_id']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('food_item', schema=None) as batch_op: + batch_op.drop_constraint('name_owner_key', type_='unique') + batch_op.create_unique_constraint(batch_op.f('barcode_owner_key'), ['barcode', 'owner_id']) + + # ### end Alembic commands ### diff --git a/migrations/versions/f5fbbe915d51_.py b/migrations/versions/f5fbbe915d51_.py new file mode 100644 index 0000000..286936b --- /dev/null +++ b/migrations/versions/f5fbbe915d51_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: f5fbbe915d51 +Revises: 99f86450e4af +Create Date: 2025-08-11 12:37:02.407643 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f5fbbe915d51' +down_revision = '99f86450e4af' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('food_item', schema=None) as batch_op: + batch_op.create_unique_constraint('barcode_owner_key', ['barcode', 'owner_id']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('food_item', schema=None) as batch_op: + batch_op.drop_constraint('barcode_owner_key', type_='unique') + + # ### end Alembic commands ### diff --git a/models.py b/models.py index d3b7500..d1c8d49 100644 --- a/models.py +++ b/models.py @@ -52,10 +52,16 @@ class FoodItem(db.Model): fat_100 = db.Column(db.Float, nullable=False) saturated_fat_100 = db.Column(db.Float) - food_logs = db.relationship("FoodLog", backref="food_item", lazy="dynamic") + food_logs = db.relationship( + "FoodLog", + backref="food_item", + lazy="dynamic", + cascade="all, delete-orphan", + ) __table_args__ = ( db.UniqueConstraint("barcode", "owner_id", name="barcode_owner_key"), + db.UniqueConstraint("name", "owner_id", name="name_owner_key"), ) def __init__(