mirror of
https://github.com/StefBuwalda/cal_counter.git
synced 2025-10-30 03:10:00 +00:00
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:
@@ -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()
|
||||
|
||||
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))
|
||||
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))
|
||||
|
||||
# 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:
|
||||
|
||||
2
forms.py
2
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()])
|
||||
|
||||
34
migrations/versions/99f86450e4af_.py
Normal file
34
migrations/versions/99f86450e4af_.py
Normal 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 ###
|
||||
32
migrations/versions/f5fbbe915d51_.py
Normal file
32
migrations/versions/f5fbbe915d51_.py
Normal 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 ###
|
||||
@@ -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__(
|
||||
|
||||
Reference in New Issue
Block a user