From d440d51ffeeea69506b8aeb338eafffcc984d0b2 Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Tue, 14 Nov 2023 09:39:07 -0600 Subject: [PATCH] feat: plural foods and units, and aliases (#2674) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added plural names and alias tables to foods/units * updated models to include plural names and aliases * updated parser to include plural and aliases * fixed migrations * fixed recursive models * added plural abbreviation to migration * updated parser and display prop * update displays to use plurals * fix display edgecase and remove print * added/updated display tests * fixed model bug and added parser tests * added tests for aliases * added new plural options to data management page * removed unique constraint * made base dialog more customizable * added alias management to food and unit data pages * removed unused awaits * 🧹 --- ...9a_added_normalized_unit_and_food_names.py | 14 +- ...6_dded3119c1fe_added_unique_constraints.py | 8 +- ...dded_plural_names_and_alias_tables_for_.py | 106 +++++++++ .../Recipe/RecipeDataAliasManagerDialog.vue | 141 ++++++++++++ frontend/components/global/BaseDialog.vue | 8 + .../recipes/use-recipe-ingredients.test.ts | 70 ++++++ .../recipes/use-recipe-ingredients.ts | 42 +++- frontend/lang/messages/en-US.json | 14 +- frontend/lib/api/types/recipe.ts | 32 ++- frontend/pages/group/data/foods.vue | 73 ++++++- frontend/pages/group/data/units.vue | 89 +++++++- mealie/db/models/recipe/ingredient.py | 195 ++++++++++++++++- mealie/schema/recipe/__init__.py | 92 ++++---- mealie/schema/recipe/recipe_ingredient.py | 71 +++++- .../parser_services/ingredient_parser.py | 56 +++-- .../test_recipe_ingredients.py | 202 ++++++++++++++++++ tests/unit_tests/test_ingredient_parser.py | 60 ++++++ 17 files changed, 1175 insertions(+), 98 deletions(-) create mode 100644 alembic/versions/2023-10-19-19.22.55_ba1e4a6cfe99_added_plural_names_and_alias_tables_for_.py create mode 100644 frontend/components/Domain/Recipe/RecipeDataAliasManagerDialog.vue create mode 100644 tests/integration_tests/user_recipe_tests/test_recipe_ingredients.py diff --git a/alembic/versions/2023-09-01-14.55.42_0341b154f79a_added_normalized_unit_and_food_names.py b/alembic/versions/2023-09-01-14.55.42_0341b154f79a_added_normalized_unit_and_food_names.py index f7b399e1dbc..44efe74faf8 100644 --- a/alembic/versions/2023-09-01-14.55.42_0341b154f79a_added_normalized_unit_and_food_names.py +++ b/alembic/versions/2023-09-01-14.55.42_0341b154f79a_added_normalized_unit_and_food_names.py @@ -22,7 +22,15 @@ def populate_normalized_fields(): bind = op.get_bind() session = orm.Session(bind=bind) - units = session.execute(select(IngredientUnitModel)).scalars().all() + units = ( + session.execute( + select(IngredientUnitModel).options( + orm.load_only(IngredientUnitModel.name, IngredientUnitModel.abbreviation) + ) + ) + .scalars() + .all() + ) for unit in units: if unit.name is not None: unit.name_normalized = IngredientUnitModel.normalize(unit.name) @@ -32,7 +40,9 @@ def populate_normalized_fields(): session.add(unit) - foods = session.execute(select(IngredientFoodModel)).scalars().all() + foods = ( + session.execute(select(IngredientFoodModel).options(orm.load_only(IngredientFoodModel.name))).scalars().all() + ) for food in foods: if food.name is not None: food.name_normalized = IngredientFoodModel.normalize(food.name) diff --git a/alembic/versions/2023-10-04-14.29.26_dded3119c1fe_added_unique_constraints.py b/alembic/versions/2023-10-04-14.29.26_dded3119c1fe_added_unique_constraints.py index 8074b906ca1..fe624cf20da 100644 --- a/alembic/versions/2023-10-04-14.29.26_dded3119c1fe_added_unique_constraints.py +++ b/alembic/versions/2023-10-04-14.29.26_dded3119c1fe_added_unique_constraints.py @@ -10,7 +10,7 @@ from typing import Any import sqlalchemy as sa -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, load_only import mealie.db.migration_types from alembic import op @@ -44,7 +44,7 @@ def _is_postgres(): def _get_duplicates(session: Session, model: SqlAlchemyBase) -> defaultdict[str, list[str]]: duplicate_map: defaultdict[str, list[str]] = defaultdict(list) - for obj in session.query(model).all(): + for obj in session.query(model).options(load_only(model.id, model.group_id, model.name)).all(): key = f"{obj.group_id}$${obj.name}" duplicate_map[key].append(str(obj.id)) @@ -117,9 +117,9 @@ def _resolve_duplivate_foods_units_labels(): continue keep_id = ids[0] - keep_obj = session.query(model).filter_by(id=keep_id).first() + keep_obj = session.query(model).options(load_only(model.id)).filter_by(id=keep_id).first() for dupe_id in ids[1:]: - dupe_obj = session.query(model).filter_by(id=dupe_id).first() + dupe_obj = session.query(model).options(load_only(model.id)).filter_by(id=dupe_id).first() resolve_func(session, keep_obj, dupe_obj) diff --git a/alembic/versions/2023-10-19-19.22.55_ba1e4a6cfe99_added_plural_names_and_alias_tables_for_.py b/alembic/versions/2023-10-19-19.22.55_ba1e4a6cfe99_added_plural_names_and_alias_tables_for_.py new file mode 100644 index 00000000000..23952dc704a --- /dev/null +++ b/alembic/versions/2023-10-19-19.22.55_ba1e4a6cfe99_added_plural_names_and_alias_tables_for_.py @@ -0,0 +1,106 @@ +"""added plural names and alias tables for foods and units + +Revision ID: ba1e4a6cfe99 +Revises: dded3119c1fe +Create Date: 2023-10-19 19:22:55.369319 + +""" +import sqlalchemy as sa + +import mealie.db.migration_types +from alembic import op + +# revision identifiers, used by Alembic. +revision = "ba1e4a6cfe99" +down_revision = "dded3119c1fe" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "ingredient_units_aliases", + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("unit_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("name_normalized", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["unit_id"], + ["ingredient_units.id"], + ), + sa.PrimaryKeyConstraint("id", "unit_id"), + ) + op.create_index( + op.f("ix_ingredient_units_aliases_created_at"), "ingredient_units_aliases", ["created_at"], unique=False + ) + op.create_index( + op.f("ix_ingredient_units_aliases_name_normalized"), + "ingredient_units_aliases", + ["name_normalized"], + unique=False, + ) + op.create_table( + "ingredient_foods_aliases", + sa.Column("id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("food_id", mealie.db.migration_types.GUID(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("name_normalized", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("update_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["food_id"], + ["ingredient_foods.id"], + ), + sa.PrimaryKeyConstraint("id", "food_id"), + ) + op.create_index( + op.f("ix_ingredient_foods_aliases_created_at"), "ingredient_foods_aliases", ["created_at"], unique=False + ) + op.create_index( + op.f("ix_ingredient_foods_aliases_name_normalized"), + "ingredient_foods_aliases", + ["name_normalized"], + unique=False, + ) + op.add_column("ingredient_foods", sa.Column("plural_name", sa.String(), nullable=True)) + op.add_column("ingredient_foods", sa.Column("plural_name_normalized", sa.String(), nullable=True)) + op.create_index( + op.f("ix_ingredient_foods_plural_name_normalized"), "ingredient_foods", ["plural_name_normalized"], unique=False + ) + op.add_column("ingredient_units", sa.Column("plural_name", sa.String(), nullable=True)) + op.add_column("ingredient_units", sa.Column("plural_name_normalized", sa.String(), nullable=True)) + op.create_index( + op.f("ix_ingredient_units_plural_name_normalized"), "ingredient_units", ["plural_name_normalized"], unique=False + ) + op.add_column("ingredient_units", sa.Column("plural_abbreviation", sa.String(), nullable=True)) + op.add_column("ingredient_units", sa.Column("plural_abbreviation_normalized", sa.String(), nullable=True)) + op.create_index( + op.f("ix_ingredient_units_plural_abbreviation_normalized"), + "ingredient_units", + ["plural_abbreviation_normalized"], + unique=False, + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_ingredient_units_plural_abbreviation_normalized"), table_name="ingredient_units") + op.drop_column("ingredient_units", "plural_abbreviation_normalized") + op.drop_column("ingredient_units", "plural_abbreviation") + op.drop_index(op.f("ix_ingredient_units_plural_name_normalized"), table_name="ingredient_units") + op.drop_column("ingredient_units", "plural_name_normalized") + op.drop_column("ingredient_units", "plural_name") + op.drop_index(op.f("ix_ingredient_foods_plural_name_normalized"), table_name="ingredient_foods") + op.drop_column("ingredient_foods", "plural_name_normalized") + op.drop_column("ingredient_foods", "plural_name") + op.drop_index(op.f("ix_ingredient_foods_aliases_name_normalized"), table_name="ingredient_foods_aliases") + op.drop_index(op.f("ix_ingredient_foods_aliases_created_at"), table_name="ingredient_foods_aliases") + op.drop_table("ingredient_foods_aliases") + op.drop_index(op.f("ix_ingredient_units_aliases_name_normalized"), table_name="ingredient_units_aliases") + op.drop_index(op.f("ix_ingredient_units_aliases_created_at"), table_name="ingredient_units_aliases") + op.drop_table("ingredient_units_aliases") + # ### end Alembic commands ### diff --git a/frontend/components/Domain/Recipe/RecipeDataAliasManagerDialog.vue b/frontend/components/Domain/Recipe/RecipeDataAliasManagerDialog.vue new file mode 100644 index 00000000000..6b0741f53c4 --- /dev/null +++ b/frontend/components/Domain/Recipe/RecipeDataAliasManagerDialog.vue @@ -0,0 +1,141 @@ + + + diff --git a/frontend/components/global/BaseDialog.vue b/frontend/components/global/BaseDialog.vue index a7f17618975..30a0c9aaf38 100644 --- a/frontend/components/global/BaseDialog.vue +++ b/frontend/components/global/BaseDialog.vue @@ -58,8 +58,12 @@ {{ $t("general.confirm") }} + {{ submitText }} + @@ -109,6 +113,10 @@ export default defineComponent({ default: null, type: Boolean, }, + submitIcon: { + type: String, + default: null, + }, submitText: { type: String, default: function () { diff --git a/frontend/composables/recipes/use-recipe-ingredients.test.ts b/frontend/composables/recipes/use-recipe-ingredients.test.ts index c7e6900bea7..7850dca1d87 100644 --- a/frontend/composables/recipes/use-recipe-ingredients.test.ts +++ b/frontend/composables/recipes/use-recipe-ingredients.test.ts @@ -48,4 +48,74 @@ describe(parseIngredientText.name, () => { expect(parseIngredientText(ingredient, false)).not.toContain("