Squashed commit of the following:

commit 7fe30bfebf
Author: Stef <stbuwalda@gmail.com>
Date:   Mon Aug 11 14:32:53 2025 +0200

    Improve FoodItem uniqueness and add name constraint

    Refactored add_meal routes to check for existing FoodItems by name or barcode and improved form handling. Made barcode optional in FoodItemForm. Added a unique constraint on (name, owner_id) for FoodItem in both the model and database migrations, while retaining the (barcode, owner_id) constraint. Updated FoodItem relationship to cascade deletes.
This commit is contained in:
2025-08-11 14:33:13 +02:00
parent ad7f787ce5
commit 6ad7ffa62b
5 changed files with 134 additions and 46 deletions

View File

@@ -12,8 +12,10 @@ from flask_login import current_user
from forms import FoodItemForm, FoodLogForm from forms import FoodItemForm, FoodLogForm
from application import db from application import db
from models import FoodItem, FoodLog from models import FoodItem, FoodLog
from sqlalchemy import and_ from sqlalchemy import and_, or_
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from sqlalchemy.sql.elements import BinaryExpression
from typing import cast
bp = Blueprint( bp = Blueprint(
"add_meal", "add_meal",
@@ -51,59 +53,23 @@ def step3(input: str):
# Check if input is a barcode # Check if input is a barcode
if input.isdigit(): if input.isdigit():
item = current_user.food_items.filter_by(barcode=input).first() 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: else:
# input is not a number, must be the name of the item.
item = current_user.food_items.filter_by(name=input).first() item = current_user.food_items.filter_by(name=input).first()
if item is None:
# Does not exist, add manually. if item is None:
return redirect(url_for("add_meal.step3_alt1", input=input)) # Does not exist, add item
return redirect(url_for("add_meal.step3_alt1", input=input))
# Track item to add and continue to next step # Track item to add and continue to next step
session["item_id"] = item.id session["item_id"] = item.id
return redirect(url_for("add_meal.step4")) return redirect(url_for("add_meal.step4"))
@bp.route("/step3_alt1/<string:input>", methods=["GET", "POST"]) @bp.route("/step3_alt1/<string:input>", methods=["GET"])
def step3_alt1(input: str): def step3_alt1(input: str):
form = FoodItemForm() 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(): if input.isdigit():
form.barcode.data = input form.barcode.data = input
else: else:
@@ -111,6 +77,56 @@ def step3_alt1(input: str):
return render_template("add_item.html", form=form) return render_template("add_item.html", form=form)
@bp.route("/step3_alt1/<string:input>", 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"]) @bp.route("/step4", methods=["GET", "POST"])
def step4(): def step4():
if "item_id" not in session: if "item_id" not in session:

View File

@@ -16,7 +16,7 @@ class LoginForm(FlaskForm):
class FoodItemForm(FlaskForm): class FoodItemForm(FlaskForm):
barcode = StringField("Barcode", validators=[InputRequired()]) barcode = StringField("Barcode", validators=[Optional()])
name = StringField("Product Name", validators=[DataRequired()]) name = StringField("Product Name", validators=[DataRequired()])
energy = IntegerField("Energy per 100g", validators=[InputRequired()]) energy = IntegerField("Energy per 100g", validators=[InputRequired()])
protein = FloatField("protein per 100g", validators=[InputRequired()]) protein = FloatField("protein per 100g", validators=[InputRequired()])

View File

@@ -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 ###

View File

@@ -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 ###

View File

@@ -52,10 +52,16 @@ class FoodItem(db.Model):
fat_100 = db.Column(db.Float, nullable=False) fat_100 = db.Column(db.Float, nullable=False)
saturated_fat_100 = db.Column(db.Float) 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__ = ( __table_args__ = (
db.UniqueConstraint("barcode", "owner_id", name="barcode_owner_key"), db.UniqueConstraint("barcode", "owner_id", name="barcode_owner_key"),
db.UniqueConstraint("name", "owner_id", name="name_owner_key"),
) )
def __init__( def __init__(