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:32:53 +02:00
parent ad7f787ce5
commit 7fe30bfebf
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 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/<string:input>", methods=["GET", "POST"])
@bp.route("/step3_alt1/<string:input>", 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/<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"])
def step4():
if "item_id" not in session:

View File

@@ -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()])

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)
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__(