mirror of
https://github.com/StefBuwalda/cal_counter.git
synced 2025-10-30 11:19:59 +00:00
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:
@@ -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:
|
||||||
|
|||||||
2
forms.py
2
forms.py
@@ -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()])
|
||||||
|
|||||||
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)
|
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__(
|
||||||
|
|||||||
Reference in New Issue
Block a user