mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-06-23 15:31:37 -04:00
feat: plural foods and units, and aliases (#2674)
* 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
* 🧹
This commit is contained in:
parent
4b55b838ed
commit
d440d51ffe
@ -22,7 +22,15 @@ def populate_normalized_fields():
|
|||||||
bind = op.get_bind()
|
bind = op.get_bind()
|
||||||
session = orm.Session(bind=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:
|
for unit in units:
|
||||||
if unit.name is not None:
|
if unit.name is not None:
|
||||||
unit.name_normalized = IngredientUnitModel.normalize(unit.name)
|
unit.name_normalized = IngredientUnitModel.normalize(unit.name)
|
||||||
@ -32,7 +40,9 @@ def populate_normalized_fields():
|
|||||||
|
|
||||||
session.add(unit)
|
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:
|
for food in foods:
|
||||||
if food.name is not None:
|
if food.name is not None:
|
||||||
food.name_normalized = IngredientFoodModel.normalize(food.name)
|
food.name_normalized = IngredientFoodModel.normalize(food.name)
|
||||||
|
@ -10,7 +10,7 @@ from dataclasses import dataclass
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session, load_only
|
||||||
|
|
||||||
import mealie.db.migration_types
|
import mealie.db.migration_types
|
||||||
from alembic import op
|
from alembic import op
|
||||||
@ -44,7 +44,7 @@ def _is_postgres():
|
|||||||
|
|
||||||
def _get_duplicates(session: Session, model: SqlAlchemyBase) -> defaultdict[str, list[str]]:
|
def _get_duplicates(session: Session, model: SqlAlchemyBase) -> defaultdict[str, list[str]]:
|
||||||
duplicate_map: defaultdict[str, list[str]] = defaultdict(list)
|
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}"
|
key = f"{obj.group_id}$${obj.name}"
|
||||||
duplicate_map[key].append(str(obj.id))
|
duplicate_map[key].append(str(obj.id))
|
||||||
|
|
||||||
@ -117,9 +117,9 @@ def _resolve_duplivate_foods_units_labels():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
keep_id = ids[0]
|
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:]:
|
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)
|
resolve_func(session, keep_obj, dupe_obj)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 ###
|
@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<BaseDialog
|
||||||
|
v-model="dialog"
|
||||||
|
:title="$t('data-pages.manage-aliases')"
|
||||||
|
:icon="$globals.icons.edit"
|
||||||
|
:submit-icon="$globals.icons.check"
|
||||||
|
:submit-text="$tc('general.confirm')"
|
||||||
|
@submit="saveAliases"
|
||||||
|
@cancel="$emit('cancel')"
|
||||||
|
>
|
||||||
|
<v-card-text>
|
||||||
|
<v-container>
|
||||||
|
<v-row v-for="alias, i in aliases" :key="i">
|
||||||
|
<v-col cols="10">
|
||||||
|
<v-text-field
|
||||||
|
v-model="alias.name"
|
||||||
|
:label="$t('general.name')"
|
||||||
|
:rules="[validators.required]"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="2">
|
||||||
|
<BaseButtonGroup
|
||||||
|
:buttons="[
|
||||||
|
{
|
||||||
|
icon: $globals.icons.delete,
|
||||||
|
text: $tc('general.delete'),
|
||||||
|
event: 'delete'
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
@delete="deleteAlias(i)"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-card-text>
|
||||||
|
<template #custom-card-action>
|
||||||
|
<BaseButton edit @click="createAlias">{{ $t('data-pages.create-alias') }}
|
||||||
|
<template #icon>
|
||||||
|
{{ $globals.icons.create }}
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</template>
|
||||||
|
</BaseDialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, ref } from "@nuxtjs/composition-api";
|
||||||
|
import { whenever } from "@vueuse/core";
|
||||||
|
import { validators } from "~/composables/use-validators";
|
||||||
|
import { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||||
|
|
||||||
|
export interface GenericAlias {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object as () => IngredientFood | IngredientUnit,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, context) {
|
||||||
|
// V-Model Support
|
||||||
|
const dialog = computed({
|
||||||
|
get: () => {
|
||||||
|
return props.value;
|
||||||
|
},
|
||||||
|
set: (val) => {
|
||||||
|
context.emit("input", val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function createAlias() {
|
||||||
|
aliases.value.push({
|
||||||
|
"name": "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteAlias(index: number) {
|
||||||
|
aliases.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const aliases = ref<GenericAlias[]>(props.data.aliases || []);
|
||||||
|
function initAliases() {
|
||||||
|
aliases.value = [...props.data.aliases || []];
|
||||||
|
if (!aliases.value.length) {
|
||||||
|
createAlias();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initAliases();
|
||||||
|
whenever(
|
||||||
|
() => props.value,
|
||||||
|
() => {
|
||||||
|
initAliases();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function saveAliases() {
|
||||||
|
const seenAliasNames: string[] = [];
|
||||||
|
const keepAliases: GenericAlias[] = [];
|
||||||
|
aliases.value.forEach((alias) => {
|
||||||
|
if (
|
||||||
|
!alias.name
|
||||||
|
|| alias.name === props.data.name
|
||||||
|
|| alias.name === props.data.pluralName
|
||||||
|
// @ts-ignore only applies to units
|
||||||
|
|| alias.name === props.data.abbreviation
|
||||||
|
// @ts-ignore only applies to units
|
||||||
|
|| alias.name === props.data.pluralAbbreviation
|
||||||
|
|| seenAliasNames.includes(alias.name)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
keepAliases.push(alias);
|
||||||
|
seenAliasNames.push(alias.name);
|
||||||
|
})
|
||||||
|
|
||||||
|
aliases.value = keepAliases;
|
||||||
|
context.emit("submit", keepAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
aliases,
|
||||||
|
createAlias,
|
||||||
|
dialog,
|
||||||
|
deleteAlias,
|
||||||
|
saveAliases,
|
||||||
|
validators,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@ -58,8 +58,12 @@
|
|||||||
</template>
|
</template>
|
||||||
{{ $t("general.confirm") }}
|
{{ $t("general.confirm") }}
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
<slot name="custom-card-action"></slot>
|
||||||
<BaseButton v-if="$listeners.submit" type="submit" @click="submitEvent">
|
<BaseButton v-if="$listeners.submit" type="submit" @click="submitEvent">
|
||||||
{{ submitText }}
|
{{ submitText }}
|
||||||
|
<template v-if="submitIcon" #icon>
|
||||||
|
{{ submitIcon }}
|
||||||
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</slot>
|
</slot>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
@ -109,6 +113,10 @@ export default defineComponent({
|
|||||||
default: null,
|
default: null,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
submitIcon: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
submitText: {
|
submitText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: function () {
|
default: function () {
|
||||||
|
@ -48,4 +48,74 @@ describe(parseIngredientText.name, () => {
|
|||||||
|
|
||||||
expect(parseIngredientText(ingredient, false)).not.toContain("<script>");
|
expect(parseIngredientText(ingredient, false)).not.toContain("<script>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("plural test : plural qty : use abbreviation", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 2,
|
||||||
|
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: true },
|
||||||
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseIngredientText(ingredient, false)).toEqual("2 tbsps diced onions");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plural test : plural qty : not abbreviation", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 2,
|
||||||
|
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: false },
|
||||||
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseIngredientText(ingredient, false)).toEqual("2 tablespoons diced onions");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plural test : single qty : use abbreviation", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 1,
|
||||||
|
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: true },
|
||||||
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseIngredientText(ingredient, false)).toEqual("1 tbsp diced onion");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plural test : single qty : not abbreviation", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 1,
|
||||||
|
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: false },
|
||||||
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseIngredientText(ingredient, false)).toEqual("1 tablespoon diced onion");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plural test : small qty : use abbreviation", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 0.5,
|
||||||
|
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: true },
|
||||||
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseIngredientText(ingredient, false)).toEqual("0.5 tbsp diced onion");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plural test : small qty : not abbreviation", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 0.5,
|
||||||
|
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: false },
|
||||||
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseIngredientText(ingredient, false)).toEqual("0.5 tablespoon diced onion");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("plural test : zero qty", () => {
|
||||||
|
const ingredient = createRecipeIngredient({
|
||||||
|
quantity: 0,
|
||||||
|
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: false },
|
||||||
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseIngredientText(ingredient, false)).toEqual("diced onions");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import DOMPurify from "isomorphic-dompurify";
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
import { useFraction } from "./use-fraction";
|
import { useFraction } from "./use-fraction";
|
||||||
import { RecipeIngredient } from "~/lib/api/types/recipe";
|
import { CreateIngredientFood, CreateIngredientUnit, IngredientFood, IngredientUnit, RecipeIngredient } from "~/lib/api/types/recipe";
|
||||||
const { frac } = useFraction();
|
const { frac } = useFraction();
|
||||||
|
|
||||||
export function sanitizeIngredientHTML(rawHtml: string) {
|
export function sanitizeIngredientHTML(rawHtml: string) {
|
||||||
@ -10,6 +10,31 @@ export function sanitizeIngredientHTML(rawHtml: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useFoodName(food: CreateIngredientFood | IngredientFood | undefined, usePlural: boolean) {
|
||||||
|
if (!food) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (usePlural ? food.pluralName || food.name : food.name) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function useUnitName(unit: CreateIngredientUnit | IngredientUnit | undefined, usePlural: boolean) {
|
||||||
|
if (!unit) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let returnVal = "";
|
||||||
|
if (unit.useAbbreviation) {
|
||||||
|
returnVal = (usePlural ? unit.pluralAbbreviation || unit.abbreviation : unit.abbreviation) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!returnVal) {
|
||||||
|
returnVal = (usePlural ? unit.pluralName || unit.name : unit.name) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnVal;
|
||||||
|
}
|
||||||
|
|
||||||
export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmount: boolean, scale = 1, includeFormating = true) {
|
export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmount: boolean, scale = 1, includeFormating = true) {
|
||||||
if (disableAmount) {
|
if (disableAmount) {
|
||||||
return {
|
return {
|
||||||
@ -21,11 +46,11 @@ export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmo
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { quantity, food, unit, note } = ingredient;
|
const { quantity, food, unit, note } = ingredient;
|
||||||
|
const usePluralUnit = quantity !== undefined && quantity > 1;
|
||||||
|
const usePluralFood = (!quantity) || quantity > 1
|
||||||
|
|
||||||
let returnQty = "";
|
let returnQty = "";
|
||||||
|
|
||||||
let unitDisplay = unit?.name;
|
|
||||||
|
|
||||||
// casting to number is required as sometimes quantity is a string
|
// casting to number is required as sometimes quantity is a string
|
||||||
if (quantity && Number(quantity) !== 0) {
|
if (quantity && Number(quantity) !== 0) {
|
||||||
if (unit?.fraction) {
|
if (unit?.fraction) {
|
||||||
@ -42,16 +67,15 @@ export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmo
|
|||||||
} else {
|
} else {
|
||||||
returnQty = (quantity * scale).toString();
|
returnQty = (quantity * scale).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unit?.useAbbreviation && unit.abbreviation) {
|
|
||||||
unitDisplay = unit.abbreviation;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unitName = useUnitName(unit, usePluralUnit);
|
||||||
|
const foodName = useFoodName(food, usePluralFood);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
quantity: returnQty ? sanitizeIngredientHTML(returnQty) : undefined,
|
quantity: returnQty ? sanitizeIngredientHTML(returnQty) : undefined,
|
||||||
unit: unitDisplay ? sanitizeIngredientHTML(unitDisplay) : undefined,
|
unit: unitName && quantity ? sanitizeIngredientHTML(unitName) : undefined,
|
||||||
name: food?.name ? sanitizeIngredientHTML(food.name) : undefined,
|
name: foodName ? sanitizeIngredientHTML(foodName) : undefined,
|
||||||
note: note ? sanitizeIngredientHTML(note) : undefined,
|
note: note ? sanitizeIngredientHTML(note) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -124,6 +124,7 @@
|
|||||||
"no-recipe-found": "No Recipe Found",
|
"no-recipe-found": "No Recipe Found",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"options": "Options:",
|
"options": "Options:",
|
||||||
|
"plural-name": "Plural Name",
|
||||||
"print": "Print",
|
"print": "Print",
|
||||||
"print-preferences": "Print Preferences",
|
"print-preferences": "Print Preferences",
|
||||||
"random": "Random",
|
"random": "Random",
|
||||||
@ -888,7 +889,9 @@
|
|||||||
"create-food": "Create Food",
|
"create-food": "Create Food",
|
||||||
"food-label": "Food Label",
|
"food-label": "Food Label",
|
||||||
"edit-food": "Edit Food",
|
"edit-food": "Edit Food",
|
||||||
"food-data": "Food Data"
|
"food-data": "Food Data",
|
||||||
|
"example-food-singular": "ex: Onion",
|
||||||
|
"example-food-plural": "ex: Onions"
|
||||||
},
|
},
|
||||||
"units": {
|
"units": {
|
||||||
"seed-dialog-text": "Seed the database with common units based on your local language.",
|
"seed-dialog-text": "Seed the database with common units based on your local language.",
|
||||||
@ -899,13 +902,18 @@
|
|||||||
"merging-unit-into-unit": "Merging {0} into {1}",
|
"merging-unit-into-unit": "Merging {0} into {1}",
|
||||||
"create-unit": "Create Unit",
|
"create-unit": "Create Unit",
|
||||||
"abbreviation": "Abbreviation",
|
"abbreviation": "Abbreviation",
|
||||||
|
"plural-abbreviation": "Plural Abbreviation",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"display-as-fraction": "Display as Fraction",
|
"display-as-fraction": "Display as Fraction",
|
||||||
"use-abbreviation": "Use Abbreviation",
|
"use-abbreviation": "Use Abbreviation",
|
||||||
"edit-unit": "Edit Unit",
|
"edit-unit": "Edit Unit",
|
||||||
"unit-data": "Unit Data",
|
"unit-data": "Unit Data",
|
||||||
"use-abbv": "Use Abbv.",
|
"use-abbv": "Use Abbv.",
|
||||||
"fraction": "Fraction"
|
"fraction": "Fraction",
|
||||||
|
"example-unit-singular": "ex: Tablespoon",
|
||||||
|
"example-unit-plural": "ex: Tablespoons",
|
||||||
|
"example-unit-abbreviation-singular": "ex: Tbsp",
|
||||||
|
"example-unit-abbreviation-plural": "ex: Tbsps"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"seed-dialog-text": "Seed the database with common labels based on your local language.",
|
"seed-dialog-text": "Seed the database with common labels based on your local language.",
|
||||||
@ -934,6 +942,8 @@
|
|||||||
"delete-recipes": "Delete Recipes",
|
"delete-recipes": "Delete Recipes",
|
||||||
"source-unit-will-be-deleted": "Source Unit will be deleted"
|
"source-unit-will-be-deleted": "Source Unit will be deleted"
|
||||||
},
|
},
|
||||||
|
"create-alias": "Create Alias",
|
||||||
|
"manage-aliases": "Manage Aliases",
|
||||||
"seed-data": "Seed Data",
|
"seed-data": "Seed Data",
|
||||||
"seed": "Seed",
|
"seed": "Seed",
|
||||||
"data-management": "Data Management",
|
"data-management": "Data Management",
|
||||||
|
@ -56,21 +56,32 @@ export interface CategorySave {
|
|||||||
}
|
}
|
||||||
export interface CreateIngredientFood {
|
export interface CreateIngredientFood {
|
||||||
name: string;
|
name: string;
|
||||||
|
pluralName?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
extras?: {
|
extras?: {
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
};
|
};
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
|
aliases?: CreateIngredientFoodAlias[];
|
||||||
|
}
|
||||||
|
export interface CreateIngredientFoodAlias {
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
export interface CreateIngredientUnit {
|
export interface CreateIngredientUnit {
|
||||||
name: string;
|
name: string;
|
||||||
|
pluralName?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
extras?: {
|
extras?: {
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
};
|
};
|
||||||
fraction?: boolean;
|
fraction?: boolean;
|
||||||
abbreviation?: string;
|
abbreviation?: string;
|
||||||
|
pluralAbbreviation?: string;
|
||||||
useAbbreviation?: boolean;
|
useAbbreviation?: boolean;
|
||||||
|
aliases?: CreateIngredientUnitAlias[];
|
||||||
|
}
|
||||||
|
export interface CreateIngredientUnitAlias {
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
export interface CreateRecipe {
|
export interface CreateRecipe {
|
||||||
name: string;
|
name: string;
|
||||||
@ -113,16 +124,21 @@ export interface IngredientConfidence {
|
|||||||
}
|
}
|
||||||
export interface IngredientFood {
|
export interface IngredientFood {
|
||||||
name: string;
|
name: string;
|
||||||
|
pluralName?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
extras?: {
|
extras?: {
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
};
|
};
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
|
aliases?: IngredientFoodAlias[];
|
||||||
id: string;
|
id: string;
|
||||||
label?: MultiPurposeLabelSummary;
|
label?: MultiPurposeLabelSummary;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updateAt?: string;
|
updateAt?: string;
|
||||||
}
|
}
|
||||||
|
export interface IngredientFoodAlias {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
export interface MultiPurposeLabelSummary {
|
export interface MultiPurposeLabelSummary {
|
||||||
name: string;
|
name: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
@ -141,17 +157,23 @@ export interface IngredientRequest {
|
|||||||
}
|
}
|
||||||
export interface IngredientUnit {
|
export interface IngredientUnit {
|
||||||
name: string;
|
name: string;
|
||||||
|
pluralName?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
extras?: {
|
extras?: {
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
};
|
};
|
||||||
fraction?: boolean;
|
fraction?: boolean;
|
||||||
abbreviation?: string;
|
abbreviation?: string;
|
||||||
|
pluralAbbreviation?: string;
|
||||||
useAbbreviation?: boolean;
|
useAbbreviation?: boolean;
|
||||||
|
aliases?: IngredientUnitAlias[];
|
||||||
id: string;
|
id: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updateAt?: string;
|
updateAt?: string;
|
||||||
}
|
}
|
||||||
|
export interface IngredientUnitAlias {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
export interface IngredientsRequest {
|
export interface IngredientsRequest {
|
||||||
parser?: RegisteredParser & string;
|
parser?: RegisteredParser & string;
|
||||||
ingredients: string[];
|
ingredients: string[];
|
||||||
@ -206,7 +228,7 @@ export interface Recipe {
|
|||||||
recipeCategory?: RecipeCategory[];
|
recipeCategory?: RecipeCategory[];
|
||||||
tags?: RecipeTag[];
|
tags?: RecipeTag[];
|
||||||
tools?: RecipeTool[];
|
tools?: RecipeTool[];
|
||||||
rating?: number | null;
|
rating?: number;
|
||||||
orgURL?: string;
|
orgURL?: string;
|
||||||
dateAdded?: string;
|
dateAdded?: string;
|
||||||
dateUpdated?: string;
|
dateUpdated?: string;
|
||||||
@ -413,22 +435,27 @@ export interface RecipeZipTokenResponse {
|
|||||||
}
|
}
|
||||||
export interface SaveIngredientFood {
|
export interface SaveIngredientFood {
|
||||||
name: string;
|
name: string;
|
||||||
|
pluralName?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
extras?: {
|
extras?: {
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
};
|
};
|
||||||
labelId?: string;
|
labelId?: string;
|
||||||
|
aliases?: CreateIngredientFoodAlias[];
|
||||||
groupId: string;
|
groupId: string;
|
||||||
}
|
}
|
||||||
export interface SaveIngredientUnit {
|
export interface SaveIngredientUnit {
|
||||||
name: string;
|
name: string;
|
||||||
|
pluralName?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
extras?: {
|
extras?: {
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
};
|
};
|
||||||
fraction?: boolean;
|
fraction?: boolean;
|
||||||
abbreviation?: string;
|
abbreviation?: string;
|
||||||
|
pluralAbbreviation?: string;
|
||||||
useAbbreviation?: boolean;
|
useAbbreviation?: boolean;
|
||||||
|
aliases?: CreateIngredientUnitAlias[];
|
||||||
groupId: string;
|
groupId: string;
|
||||||
}
|
}
|
||||||
export interface ScrapeRecipe {
|
export interface ScrapeRecipe {
|
||||||
@ -438,7 +465,7 @@ export interface ScrapeRecipe {
|
|||||||
export interface ScrapeRecipeTest {
|
export interface ScrapeRecipeTest {
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
export interface SlugResponse {}
|
export interface SlugResponse { }
|
||||||
export interface TagIn {
|
export interface TagIn {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
@ -454,6 +481,7 @@ export interface TagSave {
|
|||||||
}
|
}
|
||||||
export interface UnitFoodBase {
|
export interface UnitFoodBase {
|
||||||
name: string;
|
name: string;
|
||||||
|
pluralName?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
extras?: {
|
extras?: {
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
v-model="createDialog"
|
v-model="createDialog"
|
||||||
:icon="$globals.icons.foods"
|
:icon="$globals.icons.foods"
|
||||||
:title="$t('data-pages.foods.create-food')"
|
:title="$t('data-pages.foods.create-food')"
|
||||||
|
:submit-icon="$globals.icons.save"
|
||||||
:submit-text="$tc('general.save')"
|
:submit-text="$tc('general.save')"
|
||||||
@submit="createFood"
|
@submit="createFood"
|
||||||
>
|
>
|
||||||
@ -68,8 +69,14 @@
|
|||||||
v-model="createTarget.name"
|
v-model="createTarget.name"
|
||||||
autofocus
|
autofocus
|
||||||
:label="$t('general.name')"
|
:label="$t('general.name')"
|
||||||
|
:hint="$t('data-pages.foods.example-food-singular')"
|
||||||
:rules="[validators.required]"
|
:rules="[validators.required]"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="createTarget.pluralName"
|
||||||
|
:label="$t('general.plural-name')"
|
||||||
|
:hint="$t('data-pages.foods.example-food-plural')"
|
||||||
|
></v-text-field>
|
||||||
<v-text-field v-model="createTarget.description" :label="$t('recipe.description')"></v-text-field>
|
<v-text-field v-model="createTarget.description" :label="$t('recipe.description')"></v-text-field>
|
||||||
<v-autocomplete
|
<v-autocomplete
|
||||||
v-model="createTarget.labelId"
|
v-model="createTarget.labelId"
|
||||||
@ -83,18 +90,41 @@
|
|||||||
</v-form> </v-card-text
|
</v-form> </v-card-text
|
||||||
></BaseDialog>
|
></BaseDialog>
|
||||||
|
|
||||||
|
<!-- Alias Sub-Dialog -->
|
||||||
|
<RecipeDataAliasManagerDialog
|
||||||
|
v-if="editTarget"
|
||||||
|
:value="aliasManagerDialog"
|
||||||
|
:data="editTarget"
|
||||||
|
@submit="updateFoodAlias"
|
||||||
|
@cancel="aliasManagerDialog = false"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Edit Dialog -->
|
<!-- Edit Dialog -->
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
v-model="editDialog"
|
v-model="editDialog"
|
||||||
:icon="$globals.icons.foods"
|
:icon="$globals.icons.foods"
|
||||||
:title="$t('data-pages.foods.edit-food')"
|
:title="$t('data-pages.foods.edit-food')"
|
||||||
|
:submit-icon="$globals.icons.save"
|
||||||
:submit-text="$tc('general.save')"
|
:submit-text="$tc('general.save')"
|
||||||
@submit="editSaveFood"
|
@submit="editSaveFood"
|
||||||
>
|
>
|
||||||
<v-card-text v-if="editTarget">
|
<v-card-text v-if="editTarget">
|
||||||
<v-form ref="domEditFoodForm">
|
<v-form ref="domEditFoodForm">
|
||||||
<v-text-field v-model="editTarget.name" :label="$t('general.name')" :rules="[validators.required]"></v-text-field>
|
<v-text-field
|
||||||
<v-text-field v-model="editTarget.description" :label="$t('recipe.description')"></v-text-field>
|
v-model="editTarget.name"
|
||||||
|
:label="$t('general.name')"
|
||||||
|
:hint="$t('data-pages.foods.example-food-singular')"
|
||||||
|
:rules="[validators.required]"
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="editTarget.pluralName"
|
||||||
|
:label="$t('general.plural-name')"
|
||||||
|
:hint="$t('data-pages.foods.example-food-plural')"
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="editTarget.description"
|
||||||
|
:label="$t('recipe.description')"
|
||||||
|
></v-text-field>
|
||||||
<v-autocomplete
|
<v-autocomplete
|
||||||
v-model="editTarget.labelId"
|
v-model="editTarget.labelId"
|
||||||
clearable
|
clearable
|
||||||
@ -104,8 +134,12 @@
|
|||||||
:label="$t('data-pages.foods.food-label')"
|
:label="$t('data-pages.foods.food-label')"
|
||||||
>
|
>
|
||||||
</v-autocomplete>
|
</v-autocomplete>
|
||||||
</v-form> </v-card-text
|
</v-form>
|
||||||
></BaseDialog>
|
</v-card-text>
|
||||||
|
<template #custom-card-action>
|
||||||
|
<BaseButton edit @click="aliasManagerEventHandler">{{ $t('data-pages.manage-aliases') }}</BaseButton>
|
||||||
|
</template>
|
||||||
|
</BaseDialog>
|
||||||
|
|
||||||
<!-- Delete Dialog -->
|
<!-- Delete Dialog -->
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
@ -156,16 +190,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, ref, computed, useContext } from "@nuxtjs/composition-api";
|
import { defineComponent, onMounted, ref, computed, useContext } from "@nuxtjs/composition-api";
|
||||||
import type { LocaleObject } from "@nuxtjs/i18n";
|
import type { LocaleObject } from "@nuxtjs/i18n";
|
||||||
|
import RecipeDataAliasManagerDialog from "~/components/Domain/Recipe/RecipeDataAliasManagerDialog.vue";
|
||||||
import { validators } from "~/composables/use-validators";
|
import { validators } from "~/composables/use-validators";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { CreateIngredientFood, IngredientFood } from "~/lib/api/types/recipe";
|
import { CreateIngredientFood, IngredientFood, IngredientFoodAlias } from "~/lib/api/types/recipe";
|
||||||
import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue";
|
import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue";
|
||||||
import { useLocales } from "~/composables/use-locales";
|
import { useLocales } from "~/composables/use-locales";
|
||||||
import { useFoodStore, useLabelStore } from "~/composables/store";
|
import { useFoodStore, useLabelStore } from "~/composables/store";
|
||||||
import { VForm } from "~/types/vuetify";
|
import { VForm } from "~/types/vuetify";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { MultiPurposeLabel },
|
|
||||||
|
components: { MultiPurposeLabel, RecipeDataAliasManagerDialog },
|
||||||
setup() {
|
setup() {
|
||||||
const userApi = useUserApi();
|
const userApi = useUserApi();
|
||||||
const { i18n } = useContext();
|
const { i18n } = useContext();
|
||||||
@ -184,6 +220,11 @@ export default defineComponent({
|
|||||||
value: "name",
|
value: "name",
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: i18n.tc("general.plural-name"),
|
||||||
|
value: "pluralName",
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: i18n.tc("recipe.description"),
|
text: i18n.tc("recipe.description"),
|
||||||
value: "description",
|
value: "description",
|
||||||
@ -264,6 +305,22 @@ export default defineComponent({
|
|||||||
deleteDialog.value = false;
|
deleteDialog.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Alias Manager
|
||||||
|
|
||||||
|
const aliasManagerDialog = ref(false);
|
||||||
|
function aliasManagerEventHandler() {
|
||||||
|
aliasManagerDialog.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFoodAlias(newAliases: IngredientFoodAlias[]) {
|
||||||
|
if (!editTarget.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editTarget.value.aliases = newAliases;
|
||||||
|
aliasManagerDialog.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Merge Foods
|
// Merge Foods
|
||||||
|
|
||||||
@ -337,6 +394,10 @@ export default defineComponent({
|
|||||||
deleteEventHandler,
|
deleteEventHandler,
|
||||||
deleteDialog,
|
deleteDialog,
|
||||||
deleteFood,
|
deleteFood,
|
||||||
|
// Alias Manager
|
||||||
|
aliasManagerDialog,
|
||||||
|
aliasManagerEventHandler,
|
||||||
|
updateFoodAlias,
|
||||||
// Merge
|
// Merge
|
||||||
canMerge,
|
canMerge,
|
||||||
mergeFoods,
|
mergeFoods,
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
v-model="createDialog"
|
v-model="createDialog"
|
||||||
:icon="$globals.icons.units"
|
:icon="$globals.icons.units"
|
||||||
:title="$t('data-pages.units.create-unit')"
|
:title="$t('data-pages.units.create-unit')"
|
||||||
|
:submit-icon="$globals.icons.save"
|
||||||
:submit-text="$tc('general.save')"
|
:submit-text="$tc('general.save')"
|
||||||
@submit="createUnit"
|
@submit="createUnit"
|
||||||
>
|
>
|
||||||
@ -38,9 +39,24 @@
|
|||||||
v-model="createTarget.name"
|
v-model="createTarget.name"
|
||||||
autofocus
|
autofocus
|
||||||
:label="$t('general.name')"
|
:label="$t('general.name')"
|
||||||
|
:hint="$t('data-pages.units.example-unit-singular')"
|
||||||
:rules="[validators.required]"
|
:rules="[validators.required]"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-text-field v-model="createTarget.abbreviation" :label="$t('data-pages.units.abbreviation')"></v-text-field>
|
<v-text-field
|
||||||
|
v-model="createTarget.pluralName"
|
||||||
|
:label="$t('general.plural-name')"
|
||||||
|
:hint="$t('data-pages.units.example-unit-plural')"
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="createTarget.abbreviation"
|
||||||
|
:label="$t('data-pages.units.abbreviation')"
|
||||||
|
:hint="$t('data-pages.units.example-unit-abbreviation-singular')"
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="createTarget.pluralAbbreviation"
|
||||||
|
:label="$t('data-pages.units.plural-abbreviation')"
|
||||||
|
:hint="$t('data-pages.units.example-unit-abbreviation-plural')"
|
||||||
|
></v-text-field>
|
||||||
<v-text-field v-model="createTarget.description" :label="$t('data-pages.units.description')"></v-text-field>
|
<v-text-field v-model="createTarget.description" :label="$t('data-pages.units.description')"></v-text-field>
|
||||||
<v-checkbox v-model="createTarget.fraction" hide-details :label="$t('data-pages.units.display-as-fraction')"></v-checkbox>
|
<v-checkbox v-model="createTarget.fraction" hide-details :label="$t('data-pages.units.display-as-fraction')"></v-checkbox>
|
||||||
<v-checkbox v-model="createTarget.useAbbreviation" hide-details :label="$t('data-pages.units.use-abbreviation')"></v-checkbox>
|
<v-checkbox v-model="createTarget.useAbbreviation" hide-details :label="$t('data-pages.units.use-abbreviation')"></v-checkbox>
|
||||||
@ -48,23 +64,55 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
||||||
|
<!-- Alias Sub-Dialog -->
|
||||||
|
<RecipeDataAliasManagerDialog
|
||||||
|
v-if="editTarget"
|
||||||
|
:value="aliasManagerDialog"
|
||||||
|
:data="editTarget"
|
||||||
|
@submit="updateUnitAlias"
|
||||||
|
@cancel="aliasManagerDialog = false"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Edit Dialog -->
|
<!-- Edit Dialog -->
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
v-model="editDialog"
|
v-model="editDialog"
|
||||||
:icon="$globals.icons.units"
|
:icon="$globals.icons.units"
|
||||||
:title="$t('data-pages.units.edit-unit')"
|
:title="$t('data-pages.units.edit-unit')"
|
||||||
|
:submit-icon="$globals.icons.save"
|
||||||
:submit-text="$tc('general.save')"
|
:submit-text="$tc('general.save')"
|
||||||
@submit="editSaveUnit"
|
@submit="editSaveUnit"
|
||||||
>
|
>
|
||||||
<v-card-text v-if="editTarget">
|
<v-card-text v-if="editTarget">
|
||||||
<v-form ref="domEditUnitForm">
|
<v-form ref="domEditUnitForm">
|
||||||
<v-text-field v-model="editTarget.name" :label="$t('general.name')" :rules="[validators.required]"></v-text-field>
|
<v-text-field
|
||||||
<v-text-field v-model="editTarget.abbreviation" :label="$t('data-pages.units.abbreviation')"></v-text-field>
|
v-model="editTarget.name"
|
||||||
|
:label="$t('general.name')"
|
||||||
|
:hint="$t('data-pages.units.example-unit-singular')"
|
||||||
|
:rules="[validators.required]"
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="editTarget.pluralName"
|
||||||
|
:label="$t('general.plural-name')"
|
||||||
|
:hint="$t('data-pages.units.example-unit-plural')"
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="editTarget.abbreviation"
|
||||||
|
:label="$t('data-pages.units.abbreviation')"
|
||||||
|
:hint="$t('data-pages.units.example-unit-abbreviation-singular')"
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="editTarget.pluralAbbreviation"
|
||||||
|
:label="$t('data-pages.units.plural-abbreviation')"
|
||||||
|
:hint="$t('data-pages.units.example-unit-abbreviation-plural')"
|
||||||
|
></v-text-field>
|
||||||
<v-text-field v-model="editTarget.description" :label="$t('data-pages.units.description')"></v-text-field>
|
<v-text-field v-model="editTarget.description" :label="$t('data-pages.units.description')"></v-text-field>
|
||||||
<v-checkbox v-model="editTarget.fraction" hide-details :label="$t('data-pages.units.display-as-fraction')"></v-checkbox>
|
<v-checkbox v-model="editTarget.fraction" hide-details :label="$t('data-pages.units.display-as-fraction')"></v-checkbox>
|
||||||
<v-checkbox v-model="editTarget.useAbbreviation" hide-details :label="$t('data-pages.units.use-abbreviation')"></v-checkbox>
|
<v-checkbox v-model="editTarget.useAbbreviation" hide-details :label="$t('data-pages.units.use-abbreviation')"></v-checkbox>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
<template #custom-card-action>
|
||||||
|
<BaseButton edit @click="aliasManagerEventHandler">{{ $t('data-pages.manage-aliases') }}</BaseButton>
|
||||||
|
</template>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
||||||
<!-- Delete Dialog -->
|
<!-- Delete Dialog -->
|
||||||
@ -159,14 +207,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
import { computed, defineComponent, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
||||||
import type { LocaleObject } from "@nuxtjs/i18n";
|
import type { LocaleObject } from "@nuxtjs/i18n";
|
||||||
|
import RecipeDataAliasManagerDialog from "~/components/Domain/Recipe/RecipeDataAliasManagerDialog.vue";
|
||||||
import { validators } from "~/composables/use-validators";
|
import { validators } from "~/composables/use-validators";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { CreateIngredientUnit, IngredientUnit } from "~/lib/api/types/recipe";
|
import { CreateIngredientUnit, IngredientUnit, IngredientUnitAlias } from "~/lib/api/types/recipe";
|
||||||
import { useLocales } from "~/composables/use-locales";
|
import { useLocales } from "~/composables/use-locales";
|
||||||
import { useUnitStore } from "~/composables/store";
|
import { useUnitStore } from "~/composables/store";
|
||||||
import { VForm } from "~/types/vuetify";
|
import { VForm } from "~/types/vuetify";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
components: { RecipeDataAliasManagerDialog },
|
||||||
setup() {
|
setup() {
|
||||||
const userApi = useUserApi();
|
const userApi = useUserApi();
|
||||||
const { i18n } = useContext();
|
const { i18n } = useContext();
|
||||||
@ -185,11 +235,21 @@ export default defineComponent({
|
|||||||
value: "name",
|
value: "name",
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: i18n.t("general.plural-name"),
|
||||||
|
value: "pluralName",
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: i18n.t("data-pages.units.abbreviation"),
|
text: i18n.t("data-pages.units.abbreviation"),
|
||||||
value: "abbreviation",
|
value: "abbreviation",
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: i18n.t("data-pages.units.plural-abbreviation"),
|
||||||
|
value: "pluralAbbreviation",
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: i18n.t("data-pages.units.use-abbv"),
|
text: i18n.t("data-pages.units.use-abbv"),
|
||||||
value: "useAbbreviation",
|
value: "useAbbreviation",
|
||||||
@ -278,6 +338,22 @@ export default defineComponent({
|
|||||||
deleteDialog.value = false;
|
deleteDialog.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Alias Manager
|
||||||
|
|
||||||
|
const aliasManagerDialog = ref(false);
|
||||||
|
function aliasManagerEventHandler() {
|
||||||
|
aliasManagerDialog.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUnitAlias(newAliases: IngredientUnitAlias[]) {
|
||||||
|
if (!editTarget.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editTarget.value.aliases = newAliases;
|
||||||
|
aliasManagerDialog.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Merge Units
|
// Merge Units
|
||||||
|
|
||||||
@ -345,13 +421,16 @@ export default defineComponent({
|
|||||||
deleteEventHandler,
|
deleteEventHandler,
|
||||||
deleteDialog,
|
deleteDialog,
|
||||||
deleteUnit,
|
deleteUnit,
|
||||||
|
// Alias Manager
|
||||||
|
aliasManagerDialog,
|
||||||
|
aliasManagerEventHandler,
|
||||||
|
updateUnitAlias,
|
||||||
// Merge
|
// Merge
|
||||||
canMerge,
|
canMerge,
|
||||||
mergeUnits,
|
mergeUnits,
|
||||||
mergeDialog,
|
mergeDialog,
|
||||||
fromUnit,
|
fromUnit,
|
||||||
toUnit,
|
toUnit,
|
||||||
|
|
||||||
// Seed
|
// Seed
|
||||||
seedDatabase,
|
seedDatabase,
|
||||||
locales,
|
locales,
|
||||||
|
@ -25,25 +25,44 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
|||||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="ingredient_units", foreign_keys=[group_id])
|
group: Mapped["Group"] = orm.relationship("Group", back_populates="ingredient_units", foreign_keys=[group_id])
|
||||||
|
|
||||||
name: Mapped[str | None] = mapped_column(String)
|
name: Mapped[str | None] = mapped_column(String)
|
||||||
|
plural_name: Mapped[str | None] = mapped_column(String)
|
||||||
description: Mapped[str | None] = mapped_column(String)
|
description: Mapped[str | None] = mapped_column(String)
|
||||||
abbreviation: Mapped[str | None] = mapped_column(String)
|
abbreviation: Mapped[str | None] = mapped_column(String)
|
||||||
|
plural_abbreviation: Mapped[str | None] = mapped_column(String)
|
||||||
use_abbreviation: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
use_abbreviation: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||||
fraction: Mapped[bool | None] = mapped_column(Boolean, default=True)
|
fraction: Mapped[bool | None] = mapped_column(Boolean, default=True)
|
||||||
|
|
||||||
ingredients: Mapped[list["RecipeIngredientModel"]] = orm.relationship(
|
ingredients: Mapped[list["RecipeIngredientModel"]] = orm.relationship(
|
||||||
"RecipeIngredientModel", back_populates="unit"
|
"RecipeIngredientModel", back_populates="unit"
|
||||||
)
|
)
|
||||||
|
aliases: Mapped[list["IngredientUnitAliasModel"]] = orm.relationship(
|
||||||
|
"IngredientUnitAliasModel", back_populates="unit", cascade="all, delete, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
# Automatically updated by sqlalchemy event, do not write to this manually
|
# Automatically updated by sqlalchemy event, do not write to this manually
|
||||||
name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||||
|
plural_name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||||
abbreviation_normalized: Mapped[str | None] = mapped_column(String, index=True)
|
abbreviation_normalized: Mapped[str | None] = mapped_column(String, index=True)
|
||||||
|
plural_abbreviation_normalized: Mapped[str | None] = mapped_column(String, index=True)
|
||||||
|
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, session: Session, name: str | None = None, abbreviation: str | None = None, **_) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
session: Session,
|
||||||
|
name: str | None = None,
|
||||||
|
plural_name: str | None = None,
|
||||||
|
abbreviation: str | None = None,
|
||||||
|
plural_abbreviation: str | None = None,
|
||||||
|
**_,
|
||||||
|
) -> None:
|
||||||
if name is not None:
|
if name is not None:
|
||||||
self.name_normalized = self.normalize(name)
|
self.name_normalized = self.normalize(name)
|
||||||
|
if plural_name is not None:
|
||||||
|
self.plural_name_normalized = self.normalize(plural_name)
|
||||||
if abbreviation is not None:
|
if abbreviation is not None:
|
||||||
self.abbreviation = self.normalize(abbreviation)
|
self.abbreviation_normalized = self.normalize(abbreviation)
|
||||||
|
if plural_abbreviation is not None:
|
||||||
|
self.plural_abbreviation_normalized = self.normalize(plural_abbreviation)
|
||||||
|
|
||||||
tableargs = [
|
tableargs = [
|
||||||
sa.UniqueConstraint("name", "group_id", name="ingredient_units_name_group_id_key"),
|
sa.UniqueConstraint("name", "group_id", name="ingredient_units_name_group_id_key"),
|
||||||
@ -52,11 +71,21 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
|||||||
"name_normalized",
|
"name_normalized",
|
||||||
unique=False,
|
unique=False,
|
||||||
),
|
),
|
||||||
|
sa.Index(
|
||||||
|
"ix_ingredient_units_plural_name_normalized",
|
||||||
|
"plural_name_normalized",
|
||||||
|
unique=False,
|
||||||
|
),
|
||||||
sa.Index(
|
sa.Index(
|
||||||
"ix_ingredient_units_abbreviation_normalized",
|
"ix_ingredient_units_abbreviation_normalized",
|
||||||
"abbreviation_normalized",
|
"abbreviation_normalized",
|
||||||
unique=False,
|
unique=False,
|
||||||
),
|
),
|
||||||
|
sa.Index(
|
||||||
|
"ix_ingredient_units_plural_abbreviation_normalized",
|
||||||
|
"plural_abbreviation_normalized",
|
||||||
|
unique=False,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
if session.get_bind().name == "postgresql":
|
if session.get_bind().name == "postgresql":
|
||||||
@ -71,6 +100,15 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
|||||||
"name_normalized": "gin_trgm_ops",
|
"name_normalized": "gin_trgm_ops",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
sa.Index(
|
||||||
|
"ix_ingredient_units_plural_name_normalized_gin",
|
||||||
|
"name_normalized",
|
||||||
|
unique=False,
|
||||||
|
postgresql_using="gin",
|
||||||
|
postgresql_ops={
|
||||||
|
"plural_name_normalized": "gin_trgm_ops",
|
||||||
|
},
|
||||||
|
),
|
||||||
sa.Index(
|
sa.Index(
|
||||||
"ix_ingredient_units_abbreviation_normalized_gin",
|
"ix_ingredient_units_abbreviation_normalized_gin",
|
||||||
"abbreviation_normalized",
|
"abbreviation_normalized",
|
||||||
@ -80,6 +118,15 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
|||||||
"abbreviation_normalized": "gin_trgm_ops",
|
"abbreviation_normalized": "gin_trgm_ops",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
sa.Index(
|
||||||
|
"ix_ingredient_units_plural_abbreviation_normalized_gin",
|
||||||
|
"plural_abbreviation_normalized",
|
||||||
|
unique=False,
|
||||||
|
postgresql_using="gin",
|
||||||
|
postgresql_ops={
|
||||||
|
"plural_abbreviation_normalized": "gin_trgm_ops",
|
||||||
|
},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -95,10 +142,15 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
|||||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="ingredient_foods", foreign_keys=[group_id])
|
group: Mapped["Group"] = orm.relationship("Group", back_populates="ingredient_foods", foreign_keys=[group_id])
|
||||||
|
|
||||||
name: Mapped[str | None] = mapped_column(String)
|
name: Mapped[str | None] = mapped_column(String)
|
||||||
|
plural_name: Mapped[str | None] = mapped_column(String)
|
||||||
description: Mapped[str | None] = mapped_column(String)
|
description: Mapped[str | None] = mapped_column(String)
|
||||||
|
|
||||||
ingredients: Mapped[list["RecipeIngredientModel"]] = orm.relationship(
|
ingredients: Mapped[list["RecipeIngredientModel"]] = orm.relationship(
|
||||||
"RecipeIngredientModel", back_populates="food"
|
"RecipeIngredientModel", back_populates="food"
|
||||||
)
|
)
|
||||||
|
aliases: Mapped[list["IngredientFoodAliasModel"]] = orm.relationship(
|
||||||
|
"IngredientFoodAliasModel", back_populates="food", cascade="all, delete, delete-orphan"
|
||||||
|
)
|
||||||
extras: Mapped[list[IngredientFoodExtras]] = orm.relationship("IngredientFoodExtras", cascade="all, delete-orphan")
|
extras: Mapped[list[IngredientFoodExtras]] = orm.relationship("IngredientFoodExtras", cascade="all, delete-orphan")
|
||||||
|
|
||||||
label_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"), index=True)
|
label_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"), index=True)
|
||||||
@ -106,12 +158,15 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
|||||||
|
|
||||||
# Automatically updated by sqlalchemy event, do not write to this manually
|
# Automatically updated by sqlalchemy event, do not write to this manually
|
||||||
name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||||
|
plural_name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||||
|
|
||||||
@api_extras
|
@api_extras
|
||||||
@auto_init()
|
@auto_init()
|
||||||
def __init__(self, session: Session, name: str | None = None, **_) -> None:
|
def __init__(self, session: Session, name: str | None = None, plural_name: str | None = None, **_) -> None:
|
||||||
if name is not None:
|
if name is not None:
|
||||||
self.name_normalized = self.normalize(name)
|
self.name_normalized = self.normalize(name)
|
||||||
|
if plural_name is not None:
|
||||||
|
self.plural_name_normalized = self.normalize(plural_name)
|
||||||
|
|
||||||
tableargs = [
|
tableargs = [
|
||||||
sa.UniqueConstraint("name", "group_id", name="ingredient_foods_name_group_id_key"),
|
sa.UniqueConstraint("name", "group_id", name="ingredient_foods_name_group_id_key"),
|
||||||
@ -120,6 +175,11 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
|||||||
"name_normalized",
|
"name_normalized",
|
||||||
unique=False,
|
unique=False,
|
||||||
),
|
),
|
||||||
|
sa.Index(
|
||||||
|
"ix_ingredient_foods_plural_name_normalized",
|
||||||
|
"plural_name_normalized",
|
||||||
|
unique=False,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
if session.get_bind().name == "postgresql":
|
if session.get_bind().name == "postgresql":
|
||||||
@ -133,13 +193,104 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
|||||||
postgresql_ops={
|
postgresql_ops={
|
||||||
"name_normalized": "gin_trgm_ops",
|
"name_normalized": "gin_trgm_ops",
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
|
sa.Index(
|
||||||
|
"ix_ingredient_foods_plural_name_normalized_gin",
|
||||||
|
"plural_name_normalized",
|
||||||
|
unique=False,
|
||||||
|
postgresql_using="gin",
|
||||||
|
postgresql_ops={
|
||||||
|
"plural_name_normalized": "gin_trgm_ops",
|
||||||
|
},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.__table_args__ = tuple(tableargs)
|
self.__table_args__ = tuple(tableargs)
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientUnitAliasModel(SqlAlchemyBase, BaseMixins):
|
||||||
|
__tablename__ = "ingredient_units_aliases"
|
||||||
|
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||||
|
|
||||||
|
unit_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("ingredient_units.id"), primary_key=True)
|
||||||
|
unit: Mapped["IngredientUnitModel"] = orm.relationship("IngredientUnitModel", back_populates="aliases")
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String)
|
||||||
|
|
||||||
|
# Automatically updated by sqlalchemy event, do not write to this manually
|
||||||
|
name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||||
|
|
||||||
|
@auto_init()
|
||||||
|
def __init__(self, session: Session, name: str, **_) -> None:
|
||||||
|
self.name_normalized = self.normalize(name)
|
||||||
|
tableargs = [
|
||||||
|
sa.Index(
|
||||||
|
"ix_ingredient_units_aliases_name_normalized",
|
||||||
|
"name_normalized",
|
||||||
|
unique=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
if session.get_bind().name == "postgresql":
|
||||||
|
tableargs.extend(
|
||||||
|
[
|
||||||
|
sa.Index(
|
||||||
|
"ix_ingredient_units_aliases_name_normalized_gin",
|
||||||
|
"name_normalized",
|
||||||
|
unique=False,
|
||||||
|
postgresql_using="gin",
|
||||||
|
postgresql_ops={
|
||||||
|
"name_normalized": "gin_trgm_ops",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.__table_args__ = tableargs
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientFoodAliasModel(SqlAlchemyBase, BaseMixins):
|
||||||
|
__tablename__ = "ingredient_foods_aliases"
|
||||||
|
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||||
|
|
||||||
|
food_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("ingredient_foods.id"), primary_key=True)
|
||||||
|
food: Mapped["IngredientFoodModel"] = orm.relationship("IngredientFoodModel", back_populates="aliases")
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String)
|
||||||
|
|
||||||
|
# Automatically updated by sqlalchemy event, do not write to this manually
|
||||||
|
name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||||
|
|
||||||
|
@auto_init()
|
||||||
|
def __init__(self, session: Session, name: str, **_) -> None:
|
||||||
|
self.name_normalized = self.normalize(name)
|
||||||
|
tableargs = [
|
||||||
|
sa.Index(
|
||||||
|
"ix_ingredient_foods_aliases_name_normalized",
|
||||||
|
"name_normalized",
|
||||||
|
unique=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
if session.get_bind().name == "postgresql":
|
||||||
|
tableargs.extend(
|
||||||
|
[
|
||||||
|
sa.Index(
|
||||||
|
"ix_ingredient_foods_aliases_name_normalized_gin",
|
||||||
|
"name_normalized",
|
||||||
|
unique=False,
|
||||||
|
postgresql_using="gin",
|
||||||
|
postgresql_ops={
|
||||||
|
"name_normalized": "gin_trgm_ops",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.__table_args__ = tableargs
|
||||||
|
|
||||||
|
|
||||||
class RecipeIngredientModel(SqlAlchemyBase, BaseMixins):
|
class RecipeIngredientModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "recipes_ingredients"
|
__tablename__ = "recipes_ingredients"
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
@ -221,6 +372,14 @@ def receive_unit_name(target: IngredientUnitModel, value: str | None, oldvalue,
|
|||||||
target.name_normalized = None
|
target.name_normalized = None
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(IngredientUnitModel.plural_name, "set")
|
||||||
|
def receive_plural_unit_name(target: IngredientUnitModel, value: str | None, oldvalue, initiator):
|
||||||
|
if value is not None:
|
||||||
|
target.plural_name_normalized = IngredientUnitModel.normalize(value)
|
||||||
|
else:
|
||||||
|
target.plural_name_normalized = None
|
||||||
|
|
||||||
|
|
||||||
@event.listens_for(IngredientUnitModel.abbreviation, "set")
|
@event.listens_for(IngredientUnitModel.abbreviation, "set")
|
||||||
def receive_unit_abbreviation(target: IngredientUnitModel, value: str | None, oldvalue, initiator):
|
def receive_unit_abbreviation(target: IngredientUnitModel, value: str | None, oldvalue, initiator):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
@ -229,6 +388,14 @@ def receive_unit_abbreviation(target: IngredientUnitModel, value: str | None, ol
|
|||||||
target.abbreviation_normalized = None
|
target.abbreviation_normalized = None
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(IngredientUnitModel.plural_abbreviation, "set")
|
||||||
|
def receive_unit_plural_abbreviation(target: IngredientUnitModel, value: str | None, oldvalue, initiator):
|
||||||
|
if value is not None:
|
||||||
|
target.plural_abbreviation_normalized = IngredientUnitModel.normalize(value)
|
||||||
|
else:
|
||||||
|
target.plural_abbreviation_normalized = None
|
||||||
|
|
||||||
|
|
||||||
@event.listens_for(IngredientFoodModel.name, "set")
|
@event.listens_for(IngredientFoodModel.name, "set")
|
||||||
def receive_food_name(target: IngredientFoodModel, value: str | None, oldvalue, initiator):
|
def receive_food_name(target: IngredientFoodModel, value: str | None, oldvalue, initiator):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
@ -237,6 +404,24 @@ def receive_food_name(target: IngredientFoodModel, value: str | None, oldvalue,
|
|||||||
target.name_normalized = None
|
target.name_normalized = None
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(IngredientFoodModel.plural_name, "set")
|
||||||
|
def receive_food_plural_name(target: IngredientFoodModel, value: str | None, oldvalue, initiator):
|
||||||
|
if value is not None:
|
||||||
|
target.plural_name_normalized = IngredientFoodModel.normalize(value)
|
||||||
|
else:
|
||||||
|
target.plural_name_normalized = None
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(IngredientUnitAliasModel.name, "set")
|
||||||
|
def receive_unit_alias_name(target: IngredientUnitAliasModel, value: str, oldvalue, initiator):
|
||||||
|
target.name_normalized = IngredientUnitAliasModel.normalize(value)
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(IngredientFoodAliasModel.name, "set")
|
||||||
|
def receive_food_alias_name(target: IngredientFoodAliasModel, value: str, oldvalue, initiator):
|
||||||
|
target.name_normalized = IngredientFoodAliasModel.normalize(value)
|
||||||
|
|
||||||
|
|
||||||
@event.listens_for(RecipeIngredientModel.note, "set")
|
@event.listens_for(RecipeIngredientModel.note, "set")
|
||||||
def receive_ingredient_note(target: RecipeIngredientModel, value: str | None, oldvalue, initiator):
|
def receive_ingredient_note(target: RecipeIngredientModel, value: str | None, oldvalue, initiator):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
|
@ -47,13 +47,17 @@ from .recipe_comments import (
|
|||||||
from .recipe_image_types import RecipeImageTypes
|
from .recipe_image_types import RecipeImageTypes
|
||||||
from .recipe_ingredient import (
|
from .recipe_ingredient import (
|
||||||
CreateIngredientFood,
|
CreateIngredientFood,
|
||||||
|
CreateIngredientFoodAlias,
|
||||||
CreateIngredientUnit,
|
CreateIngredientUnit,
|
||||||
|
CreateIngredientUnitAlias,
|
||||||
IngredientConfidence,
|
IngredientConfidence,
|
||||||
IngredientFood,
|
IngredientFood,
|
||||||
|
IngredientFoodAlias,
|
||||||
IngredientFoodPagination,
|
IngredientFoodPagination,
|
||||||
IngredientRequest,
|
IngredientRequest,
|
||||||
IngredientsRequest,
|
IngredientsRequest,
|
||||||
IngredientUnit,
|
IngredientUnit,
|
||||||
|
IngredientUnitAlias,
|
||||||
IngredientUnitPagination,
|
IngredientUnitPagination,
|
||||||
MergeFood,
|
MergeFood,
|
||||||
MergeUnit,
|
MergeUnit,
|
||||||
@ -88,25 +92,6 @@ __all__ = [
|
|||||||
"RecipeToolOut",
|
"RecipeToolOut",
|
||||||
"RecipeToolResponse",
|
"RecipeToolResponse",
|
||||||
"RecipeToolSave",
|
"RecipeToolSave",
|
||||||
"RecipeTimelineEventCreate",
|
|
||||||
"RecipeTimelineEventIn",
|
|
||||||
"RecipeTimelineEventOut",
|
|
||||||
"RecipeTimelineEventPagination",
|
|
||||||
"RecipeTimelineEventUpdate",
|
|
||||||
"TimelineEventImage",
|
|
||||||
"TimelineEventType",
|
|
||||||
"RecipeAsset",
|
|
||||||
"RecipeSettings",
|
|
||||||
"RecipeShareToken",
|
|
||||||
"RecipeShareTokenCreate",
|
|
||||||
"RecipeShareTokenSave",
|
|
||||||
"RecipeShareTokenSummary",
|
|
||||||
"RecipeDuplicate",
|
|
||||||
"RecipeSlug",
|
|
||||||
"RecipeZipTokenResponse",
|
|
||||||
"SlugResponse",
|
|
||||||
"UpdateImageResponse",
|
|
||||||
"RecipeNote",
|
|
||||||
"CategoryBase",
|
"CategoryBase",
|
||||||
"CategoryIn",
|
"CategoryIn",
|
||||||
"CategoryOut",
|
"CategoryOut",
|
||||||
@ -117,12 +102,6 @@ __all__ = [
|
|||||||
"TagIn",
|
"TagIn",
|
||||||
"TagOut",
|
"TagOut",
|
||||||
"TagSave",
|
"TagSave",
|
||||||
"RecipeCommentCreate",
|
|
||||||
"RecipeCommentOut",
|
|
||||||
"RecipeCommentPagination",
|
|
||||||
"RecipeCommentSave",
|
|
||||||
"RecipeCommentUpdate",
|
|
||||||
"UserBase",
|
|
||||||
"AssignCategories",
|
"AssignCategories",
|
||||||
"AssignSettings",
|
"AssignSettings",
|
||||||
"AssignTags",
|
"AssignTags",
|
||||||
@ -130,28 +109,19 @@ __all__ = [
|
|||||||
"ExportBase",
|
"ExportBase",
|
||||||
"ExportRecipes",
|
"ExportRecipes",
|
||||||
"ExportTypes",
|
"ExportTypes",
|
||||||
"IngredientReferences",
|
"RecipeShareToken",
|
||||||
"RecipeStep",
|
"RecipeShareTokenCreate",
|
||||||
|
"RecipeShareTokenSave",
|
||||||
|
"RecipeShareTokenSummary",
|
||||||
|
"ScrapeRecipe",
|
||||||
|
"ScrapeRecipeTest",
|
||||||
|
"RecipeCommentCreate",
|
||||||
|
"RecipeCommentOut",
|
||||||
|
"RecipeCommentPagination",
|
||||||
|
"RecipeCommentSave",
|
||||||
|
"RecipeCommentUpdate",
|
||||||
|
"UserBase",
|
||||||
"RecipeImageTypes",
|
"RecipeImageTypes",
|
||||||
"Nutrition",
|
|
||||||
"CreateIngredientFood",
|
|
||||||
"CreateIngredientUnit",
|
|
||||||
"IngredientConfidence",
|
|
||||||
"IngredientFood",
|
|
||||||
"IngredientFoodPagination",
|
|
||||||
"IngredientRequest",
|
|
||||||
"IngredientUnit",
|
|
||||||
"IngredientUnitPagination",
|
|
||||||
"IngredientsRequest",
|
|
||||||
"MergeFood",
|
|
||||||
"MergeUnit",
|
|
||||||
"ParsedIngredient",
|
|
||||||
"RecipeIngredient",
|
|
||||||
"RecipeIngredientBase",
|
|
||||||
"RegisteredParser",
|
|
||||||
"SaveIngredientFood",
|
|
||||||
"SaveIngredientUnit",
|
|
||||||
"UnitFoodBase",
|
|
||||||
"CreateRecipe",
|
"CreateRecipe",
|
||||||
"CreateRecipeBulk",
|
"CreateRecipeBulk",
|
||||||
"CreateRecipeByUrlBulk",
|
"CreateRecipeByUrlBulk",
|
||||||
@ -165,6 +135,44 @@ __all__ = [
|
|||||||
"RecipeTagPagination",
|
"RecipeTagPagination",
|
||||||
"RecipeTool",
|
"RecipeTool",
|
||||||
"RecipeToolPagination",
|
"RecipeToolPagination",
|
||||||
"ScrapeRecipe",
|
"IngredientReferences",
|
||||||
"ScrapeRecipeTest",
|
"RecipeStep",
|
||||||
|
"CreateIngredientFood",
|
||||||
|
"CreateIngredientFoodAlias",
|
||||||
|
"CreateIngredientUnit",
|
||||||
|
"CreateIngredientUnitAlias",
|
||||||
|
"IngredientConfidence",
|
||||||
|
"IngredientFood",
|
||||||
|
"IngredientFoodAlias",
|
||||||
|
"IngredientFoodPagination",
|
||||||
|
"IngredientRequest",
|
||||||
|
"IngredientUnit",
|
||||||
|
"IngredientUnitAlias",
|
||||||
|
"IngredientUnitPagination",
|
||||||
|
"IngredientsRequest",
|
||||||
|
"MergeFood",
|
||||||
|
"MergeUnit",
|
||||||
|
"ParsedIngredient",
|
||||||
|
"RecipeIngredient",
|
||||||
|
"RecipeIngredientBase",
|
||||||
|
"RegisteredParser",
|
||||||
|
"SaveIngredientFood",
|
||||||
|
"SaveIngredientUnit",
|
||||||
|
"UnitFoodBase",
|
||||||
|
"RecipeAsset",
|
||||||
|
"RecipeTimelineEventCreate",
|
||||||
|
"RecipeTimelineEventIn",
|
||||||
|
"RecipeTimelineEventOut",
|
||||||
|
"RecipeTimelineEventPagination",
|
||||||
|
"RecipeTimelineEventUpdate",
|
||||||
|
"TimelineEventImage",
|
||||||
|
"TimelineEventType",
|
||||||
|
"RecipeDuplicate",
|
||||||
|
"RecipeSlug",
|
||||||
|
"RecipeZipTokenResponse",
|
||||||
|
"SlugResponse",
|
||||||
|
"UpdateImageResponse",
|
||||||
|
"Nutrition",
|
||||||
|
"RecipeSettings",
|
||||||
|
"RecipeNote",
|
||||||
]
|
]
|
||||||
|
@ -33,12 +33,23 @@ def display_fraction(fraction: Fraction):
|
|||||||
|
|
||||||
class UnitFoodBase(MealieModel):
|
class UnitFoodBase(MealieModel):
|
||||||
name: str
|
name: str
|
||||||
|
plural_name: str | None = None
|
||||||
description: str = ""
|
description: str = ""
|
||||||
extras: dict | None = {}
|
extras: dict | None = {}
|
||||||
|
|
||||||
|
|
||||||
|
class CreateIngredientFoodAlias(MealieModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientFoodAlias(CreateIngredientFoodAlias):
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
class CreateIngredientFood(UnitFoodBase):
|
class CreateIngredientFood(UnitFoodBase):
|
||||||
label_id: UUID4 | None = None
|
label_id: UUID4 | None = None
|
||||||
|
aliases: list[CreateIngredientFoodAlias] = []
|
||||||
|
|
||||||
|
|
||||||
class SaveIngredientFood(CreateIngredientFood):
|
class SaveIngredientFood(CreateIngredientFood):
|
||||||
@ -48,10 +59,12 @@ class SaveIngredientFood(CreateIngredientFood):
|
|||||||
class IngredientFood(CreateIngredientFood):
|
class IngredientFood(CreateIngredientFood):
|
||||||
id: UUID4
|
id: UUID4
|
||||||
label: MultiPurposeLabelSummary | None = None
|
label: MultiPurposeLabelSummary | None = None
|
||||||
|
aliases: list[IngredientFoodAlias] = []
|
||||||
|
|
||||||
created_at: datetime.datetime | None
|
created_at: datetime.datetime | None
|
||||||
update_at: datetime.datetime | None
|
update_at: datetime.datetime | None
|
||||||
|
|
||||||
_searchable_properties: ClassVar[list[str]] = ["name_normalized"]
|
_searchable_properties: ClassVar[list[str]] = ["name_normalized", "plural_name_normalized"]
|
||||||
_normalize_search: ClassVar[bool] = True
|
_normalize_search: ClassVar[bool] = True
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@ -67,10 +80,21 @@ class IngredientFoodPagination(PaginationBase):
|
|||||||
items: list[IngredientFood]
|
items: list[IngredientFood]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateIngredientUnitAlias(MealieModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientUnitAlias(CreateIngredientUnitAlias):
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
class CreateIngredientUnit(UnitFoodBase):
|
class CreateIngredientUnit(UnitFoodBase):
|
||||||
fraction: bool = True
|
fraction: bool = True
|
||||||
abbreviation: str = ""
|
abbreviation: str = ""
|
||||||
|
plural_abbreviation: str | None = ""
|
||||||
use_abbreviation: bool = False
|
use_abbreviation: bool = False
|
||||||
|
aliases: list[CreateIngredientUnitAlias] = []
|
||||||
|
|
||||||
|
|
||||||
class SaveIngredientUnit(CreateIngredientUnit):
|
class SaveIngredientUnit(CreateIngredientUnit):
|
||||||
@ -79,10 +103,17 @@ class SaveIngredientUnit(CreateIngredientUnit):
|
|||||||
|
|
||||||
class IngredientUnit(CreateIngredientUnit):
|
class IngredientUnit(CreateIngredientUnit):
|
||||||
id: UUID4
|
id: UUID4
|
||||||
|
aliases: list[IngredientUnitAlias] = []
|
||||||
|
|
||||||
created_at: datetime.datetime | None
|
created_at: datetime.datetime | None
|
||||||
update_at: datetime.datetime | None
|
update_at: datetime.datetime | None
|
||||||
|
|
||||||
_searchable_properties: ClassVar[list[str]] = ["name_normalized", "abbreviation_normalized"]
|
_searchable_properties: ClassVar[list[str]] = [
|
||||||
|
"name_normalized",
|
||||||
|
"plural_name_normalized",
|
||||||
|
"abbreviation_normalized",
|
||||||
|
"plural_abbreviation_normalized",
|
||||||
|
]
|
||||||
_normalize_search: ClassVar[bool] = True
|
_normalize_search: ClassVar[bool] = True
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@ -165,6 +196,36 @@ class RecipeIngredientBase(MealieModel):
|
|||||||
|
|
||||||
return f"{whole_number} {display_fraction(qty)}"
|
return f"{whole_number} {display_fraction(qty)}"
|
||||||
|
|
||||||
|
def _format_unit_for_display(self) -> str:
|
||||||
|
if not self.unit:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
use_plural = self.quantity and self.quantity > 1
|
||||||
|
unit_val = ""
|
||||||
|
if self.unit.use_abbreviation:
|
||||||
|
if use_plural:
|
||||||
|
unit_val = self.unit.plural_abbreviation or self.unit.abbreviation
|
||||||
|
else:
|
||||||
|
unit_val = self.unit.abbreviation
|
||||||
|
|
||||||
|
if not unit_val:
|
||||||
|
if use_plural:
|
||||||
|
unit_val = self.unit.plural_name or self.unit.name
|
||||||
|
else:
|
||||||
|
unit_val = self.unit.name
|
||||||
|
|
||||||
|
return unit_val
|
||||||
|
|
||||||
|
def _format_food_for_display(self) -> str:
|
||||||
|
if not self.food:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
use_plural = (not self.quantity) or self.quantity > 1
|
||||||
|
if use_plural:
|
||||||
|
return self.food.plural_name or self.food.name
|
||||||
|
else:
|
||||||
|
return self.food.name
|
||||||
|
|
||||||
def _format_display(self) -> str:
|
def _format_display(self) -> str:
|
||||||
components = []
|
components = []
|
||||||
|
|
||||||
@ -183,15 +244,15 @@ class RecipeIngredientBase(MealieModel):
|
|||||||
components.append(self.note or "")
|
components.append(self.note or "")
|
||||||
else:
|
else:
|
||||||
if self.quantity and self.unit:
|
if self.quantity and self.unit:
|
||||||
components.append(self.unit.abbreviation if self.unit.use_abbreviation else self.unit.name)
|
components.append(self._format_unit_for_display())
|
||||||
|
|
||||||
if self.food:
|
if self.food:
|
||||||
components.append(self.food.name)
|
components.append(self._format_food_for_display())
|
||||||
|
|
||||||
if self.note:
|
if self.note:
|
||||||
components.append(self.note)
|
components.append(self.note)
|
||||||
|
|
||||||
return " ".join(components)
|
return " ".join(components).strip()
|
||||||
|
|
||||||
|
|
||||||
class IngredientUnitPagination(PaginationBase):
|
class IngredientUnitPagination(PaginationBase):
|
||||||
|
@ -38,36 +38,60 @@ class ABCIngredientParser(ABC):
|
|||||||
self.group_id = group_id
|
self.group_id = group_id
|
||||||
self.session = session
|
self.session = session
|
||||||
|
|
||||||
self._foods_by_name: dict[str, IngredientFood] | None = None
|
self._foods_by_alias: dict[str, IngredientFood] | None = None
|
||||||
self._units_by_name: dict[str, IngredientUnit] | None = None
|
self._units_by_alias: dict[str, IngredientUnit] | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _repos(self) -> AllRepositories:
|
def _repos(self) -> AllRepositories:
|
||||||
return get_repositories(self.session)
|
return get_repositories(self.session)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def foods_by_normalized_name(self) -> dict[str, IngredientFood]:
|
def foods_by_alias(self) -> dict[str, IngredientFood]:
|
||||||
if self._foods_by_name is None:
|
if self._foods_by_alias is None:
|
||||||
foods_repo = self._repos.ingredient_foods.by_group(self.group_id)
|
foods_repo = self._repos.ingredient_foods.by_group(self.group_id)
|
||||||
|
|
||||||
query = PaginationQuery(page=1, per_page=-1)
|
query = PaginationQuery(page=1, per_page=-1)
|
||||||
all_foods = foods_repo.page_all(query).items
|
all_foods = foods_repo.page_all(query).items
|
||||||
self._foods_by_name = {IngredientFoodModel.normalize(food.name): food for food in all_foods if food.name}
|
|
||||||
|
|
||||||
return self._foods_by_name
|
foods_by_alias: dict[str, IngredientFood] = {}
|
||||||
|
for food in all_foods:
|
||||||
|
if food.name:
|
||||||
|
foods_by_alias[IngredientFoodModel.normalize(food.name)] = food
|
||||||
|
if food.plural_name:
|
||||||
|
foods_by_alias[IngredientFoodModel.normalize(food.plural_name)] = food
|
||||||
|
|
||||||
|
for alias in food.aliases or []:
|
||||||
|
if alias.name:
|
||||||
|
foods_by_alias[IngredientFoodModel.normalize(alias.name)] = food
|
||||||
|
|
||||||
|
self._foods_by_alias = foods_by_alias
|
||||||
|
|
||||||
|
return self._foods_by_alias
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def units_by_normalized_name_or_abbreviation(self) -> dict[str, IngredientUnit]:
|
def units_by_alias(self) -> dict[str, IngredientUnit]:
|
||||||
if self._units_by_name is None:
|
if self._units_by_alias is None:
|
||||||
units_repo = self._repos.ingredient_units.by_group(self.group_id)
|
units_repo = self._repos.ingredient_units.by_group(self.group_id)
|
||||||
|
|
||||||
query = PaginationQuery(page=1, per_page=-1)
|
query = PaginationQuery(page=1, per_page=-1)
|
||||||
all_units = units_repo.page_all(query).items
|
all_units = units_repo.page_all(query).items
|
||||||
self._units_by_name = {
|
|
||||||
IngredientUnitModel.normalize(unit.name): unit for unit in all_units if unit.name
|
|
||||||
} | {IngredientUnitModel.normalize(unit.abbreviation): unit for unit in all_units if unit.abbreviation}
|
|
||||||
|
|
||||||
return self._units_by_name
|
units_by_alias: dict[str, IngredientUnit] = {}
|
||||||
|
for unit in all_units:
|
||||||
|
if unit.name:
|
||||||
|
units_by_alias[IngredientUnitModel.normalize(unit.name)] = unit
|
||||||
|
if unit.plural_name:
|
||||||
|
units_by_alias[IngredientUnitModel.normalize(unit.plural_name)] = unit
|
||||||
|
if unit.abbreviation:
|
||||||
|
units_by_alias[IngredientUnitModel.normalize(unit.abbreviation)] = unit
|
||||||
|
if unit.plural_abbreviation:
|
||||||
|
units_by_alias[IngredientUnitModel.normalize(unit.plural_abbreviation)] = unit
|
||||||
|
|
||||||
|
for alias in unit.aliases or []:
|
||||||
|
if alias.name:
|
||||||
|
units_by_alias[IngredientUnitModel.normalize(alias.name)] = unit
|
||||||
|
|
||||||
|
self._units_by_alias = units_by_alias
|
||||||
|
|
||||||
|
return self._units_by_alias
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def food_fuzzy_match_threshold(self) -> int:
|
def food_fuzzy_match_threshold(self) -> int:
|
||||||
@ -111,7 +135,7 @@ class ABCIngredientParser(ABC):
|
|||||||
match_value = IngredientFoodModel.normalize(food.name)
|
match_value = IngredientFoodModel.normalize(food.name)
|
||||||
return self.find_match(
|
return self.find_match(
|
||||||
match_value,
|
match_value,
|
||||||
store_map=self.foods_by_normalized_name,
|
store_map=self.foods_by_alias,
|
||||||
fuzzy_match_threshold=self.food_fuzzy_match_threshold,
|
fuzzy_match_threshold=self.food_fuzzy_match_threshold,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -122,7 +146,7 @@ class ABCIngredientParser(ABC):
|
|||||||
match_value = IngredientUnitModel.normalize(unit.name)
|
match_value = IngredientUnitModel.normalize(unit.name)
|
||||||
return self.find_match(
|
return self.find_match(
|
||||||
match_value,
|
match_value,
|
||||||
store_map=self.units_by_normalized_name_or_abbreviation,
|
store_map=self.units_by_alias,
|
||||||
fuzzy_match_threshold=self.unit_fuzzy_match_threshold,
|
fuzzy_match_threshold=self.unit_fuzzy_match_threshold,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -0,0 +1,202 @@
|
|||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit, RecipeIngredient
|
||||||
|
from tests.utils.factories import random_string
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
["quantity", "quantity_display_decimal", "quantity_display_fraction", "expect_plural_unit", "expect_plural_food"],
|
||||||
|
[
|
||||||
|
[0, "", "", False, True],
|
||||||
|
[0.5, "0.5", "¹/₂", False, False],
|
||||||
|
[1, "1", "1", False, False],
|
||||||
|
[1.5, "1.5", "1 ¹/₂", True, True],
|
||||||
|
[2, "2", "2", True, True],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
["unit", "expect_display_fraction", "expected_unit_singular_string", "expected_unit_plural_string"],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
IngredientUnit(
|
||||||
|
id=uuid4(),
|
||||||
|
name="tablespoon",
|
||||||
|
plural_name=None,
|
||||||
|
abbreviation="tbsp",
|
||||||
|
plural_abbreviation=None,
|
||||||
|
use_abbreviation=False,
|
||||||
|
fraction=True,
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
"tablespoon",
|
||||||
|
"tablespoon",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
IngredientUnit(
|
||||||
|
id=uuid4(),
|
||||||
|
name="tablespoon",
|
||||||
|
plural_name=None,
|
||||||
|
abbreviation="tbsp",
|
||||||
|
plural_abbreviation=None,
|
||||||
|
use_abbreviation=False,
|
||||||
|
fraction=False,
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
"tablespoon",
|
||||||
|
"tablespoon",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
IngredientUnit(
|
||||||
|
id=uuid4(),
|
||||||
|
name="tablespoon",
|
||||||
|
plural_name=None,
|
||||||
|
abbreviation="tbsp",
|
||||||
|
plural_abbreviation=None,
|
||||||
|
use_abbreviation=True,
|
||||||
|
fraction=True,
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
"tbsp",
|
||||||
|
"tbsp",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
IngredientUnit(
|
||||||
|
id=uuid4(),
|
||||||
|
name="tablespoon",
|
||||||
|
plural_name=None,
|
||||||
|
abbreviation="tbsp",
|
||||||
|
plural_abbreviation=None,
|
||||||
|
use_abbreviation=True,
|
||||||
|
fraction=False,
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
"tbsp",
|
||||||
|
"tbsp",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
IngredientUnit(
|
||||||
|
id=uuid4(),
|
||||||
|
name="tablespoon",
|
||||||
|
plural_name="tablespoons",
|
||||||
|
abbreviation="tbsp",
|
||||||
|
plural_abbreviation="tbsps",
|
||||||
|
use_abbreviation=False,
|
||||||
|
fraction=True,
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
"tablespoon",
|
||||||
|
"tablespoons",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
IngredientUnit(
|
||||||
|
id=uuid4(),
|
||||||
|
name="tablespoon",
|
||||||
|
plural_name="tablespoons",
|
||||||
|
abbreviation="tbsp",
|
||||||
|
plural_abbreviation="tbsps",
|
||||||
|
use_abbreviation=False,
|
||||||
|
fraction=False,
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
"tablespoon",
|
||||||
|
"tablespoons",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
IngredientUnit(
|
||||||
|
id=uuid4(),
|
||||||
|
name="tablespoon",
|
||||||
|
plural_name="tablespoons",
|
||||||
|
abbreviation="tbsp",
|
||||||
|
plural_abbreviation="tbsps",
|
||||||
|
use_abbreviation=True,
|
||||||
|
fraction=True,
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
"tbsp",
|
||||||
|
"tbsps",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
IngredientUnit(
|
||||||
|
id=uuid4(),
|
||||||
|
name="tablespoon",
|
||||||
|
plural_name="tablespoons",
|
||||||
|
abbreviation="tbsp",
|
||||||
|
plural_abbreviation="tbsps",
|
||||||
|
use_abbreviation=True,
|
||||||
|
fraction=False,
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
"tbsp",
|
||||||
|
"tbsps",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
["food", "expected_food_singular_string", "expected_food_plural_string"],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
IngredientFood(id=uuid4(), name="chopped onion", plural_name=None),
|
||||||
|
"chopped onion",
|
||||||
|
"chopped onion",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
IngredientFood(id=uuid4(), name="chopped onion", plural_name="chopped onions"),
|
||||||
|
"chopped onion",
|
||||||
|
"chopped onions",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize("note", ["very thin", ""])
|
||||||
|
@pytest.mark.parametrize("use_food", [True, False])
|
||||||
|
def test_ingredient_display(
|
||||||
|
quantity: float | None,
|
||||||
|
quantity_display_decimal: str,
|
||||||
|
quantity_display_fraction: str,
|
||||||
|
unit: IngredientUnit,
|
||||||
|
food: IngredientFood,
|
||||||
|
note: str,
|
||||||
|
use_food: bool,
|
||||||
|
expect_display_fraction: bool,
|
||||||
|
expect_plural_unit: bool,
|
||||||
|
expect_plural_food: bool,
|
||||||
|
expected_unit_singular_string: str,
|
||||||
|
expected_unit_plural_string: str,
|
||||||
|
expected_food_singular_string: str,
|
||||||
|
expected_food_plural_string: str,
|
||||||
|
):
|
||||||
|
expected_components = []
|
||||||
|
if use_food:
|
||||||
|
if expect_display_fraction:
|
||||||
|
expected_components.append(quantity_display_fraction)
|
||||||
|
else:
|
||||||
|
expected_components.append(quantity_display_decimal)
|
||||||
|
|
||||||
|
if quantity:
|
||||||
|
if expect_plural_unit:
|
||||||
|
expected_components.append(expected_unit_plural_string)
|
||||||
|
else:
|
||||||
|
expected_components.append(expected_unit_singular_string)
|
||||||
|
|
||||||
|
if expect_plural_food:
|
||||||
|
expected_components.append(expected_food_plural_string)
|
||||||
|
else:
|
||||||
|
expected_components.append(expected_food_singular_string)
|
||||||
|
|
||||||
|
expected_components.append(note)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if quantity != 0 and quantity != 1:
|
||||||
|
if expect_display_fraction:
|
||||||
|
expected_components.append(quantity_display_fraction)
|
||||||
|
else:
|
||||||
|
expected_components.append(quantity_display_decimal)
|
||||||
|
|
||||||
|
expected_components.append(note)
|
||||||
|
|
||||||
|
expected_display_value = " ".join(c for c in expected_components if c)
|
||||||
|
ingredient = RecipeIngredient(
|
||||||
|
quantity=quantity, unit=unit, food=food, note=note, use_food=use_food, disable_amount=not use_food
|
||||||
|
)
|
||||||
|
assert ingredient.display == expected_display_value
|
@ -9,7 +9,9 @@ from mealie.db.db_setup import session_context
|
|||||||
from mealie.repos.repository_factory import AllRepositories
|
from mealie.repos.repository_factory import AllRepositories
|
||||||
from mealie.schema.recipe.recipe_ingredient import (
|
from mealie.schema.recipe.recipe_ingredient import (
|
||||||
CreateIngredientFood,
|
CreateIngredientFood,
|
||||||
|
CreateIngredientFoodAlias,
|
||||||
CreateIngredientUnit,
|
CreateIngredientUnit,
|
||||||
|
CreateIngredientUnitAlias,
|
||||||
IngredientFood,
|
IngredientFood,
|
||||||
IngredientUnit,
|
IngredientUnit,
|
||||||
ParsedIngredient,
|
ParsedIngredient,
|
||||||
@ -67,6 +69,12 @@ def parsed_ingredient_data(
|
|||||||
SaveIngredientFood(name="fresh ginger", group_id=unique_local_group_id),
|
SaveIngredientFood(name="fresh ginger", group_id=unique_local_group_id),
|
||||||
SaveIngredientFood(name="ground ginger", group_id=unique_local_group_id),
|
SaveIngredientFood(name="ground ginger", group_id=unique_local_group_id),
|
||||||
SaveIngredientFood(name="ñör̃m̈ãl̈ĩz̈ẽm̈ẽ", group_id=unique_local_group_id),
|
SaveIngredientFood(name="ñör̃m̈ãl̈ĩz̈ẽm̈ẽ", group_id=unique_local_group_id),
|
||||||
|
SaveIngredientFood(name="PluralFoodTest", plural_name="myfoodisplural", group_id=unique_local_group_id),
|
||||||
|
SaveIngredientFood(
|
||||||
|
name="IHaveAnAlias",
|
||||||
|
group_id=unique_local_group_id,
|
||||||
|
aliases=[CreateIngredientFoodAlias(name="thisismyalias")],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,6 +94,18 @@ def parsed_ingredient_data(
|
|||||||
SaveIngredientUnit(name="Teaspoon", group_id=unique_local_group_id),
|
SaveIngredientUnit(name="Teaspoon", group_id=unique_local_group_id),
|
||||||
SaveIngredientUnit(name="Stalk", group_id=unique_local_group_id),
|
SaveIngredientUnit(name="Stalk", group_id=unique_local_group_id),
|
||||||
SaveIngredientUnit(name="My Very Long Unit Name", abbreviation="mvlun", group_id=unique_local_group_id),
|
SaveIngredientUnit(name="My Very Long Unit Name", abbreviation="mvlun", group_id=unique_local_group_id),
|
||||||
|
SaveIngredientUnit(
|
||||||
|
name="PluralUnitName",
|
||||||
|
plural_name="abc123",
|
||||||
|
abbreviation="doremiabc",
|
||||||
|
plural_abbreviation="doremi123",
|
||||||
|
group_id=unique_local_group_id,
|
||||||
|
),
|
||||||
|
SaveIngredientUnit(
|
||||||
|
name="IHaveAnAliasToo",
|
||||||
|
group_id=unique_local_group_id,
|
||||||
|
aliases=[CreateIngredientUnitAlias(name="thisismyalias")],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -267,6 +287,46 @@ def test_brute_parser(unique_user: TestUser):
|
|||||||
True,
|
True,
|
||||||
id="normalization",
|
id="normalization",
|
||||||
),
|
),
|
||||||
|
pytest.param(
|
||||||
|
build_parsed_ing(unit=None, food="myfoodisplural"),
|
||||||
|
None,
|
||||||
|
"PluralFoodTest",
|
||||||
|
False,
|
||||||
|
True,
|
||||||
|
id="plural food name",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
build_parsed_ing(unit="abc123", food=None),
|
||||||
|
"PluralUnitName",
|
||||||
|
None,
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
id="plural unit name",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
build_parsed_ing(unit="doremi123", food=None),
|
||||||
|
"PluralUnitName",
|
||||||
|
None,
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
id="plural unit abbreviation",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
build_parsed_ing(unit=None, food="thisismyalias"),
|
||||||
|
None,
|
||||||
|
"IHaveAnAlias",
|
||||||
|
False,
|
||||||
|
True,
|
||||||
|
id="food alias",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
build_parsed_ing(unit="thisismyalias", food=None),
|
||||||
|
"IHaveAnAliasToo",
|
||||||
|
None,
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
id="unit alias",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_parser_ingredient_match(
|
def test_parser_ingredient_match(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user