mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-26 17:53:12 -05:00
Compare commits
2 Commits
feat/stand
...
mealie-nex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
189e98fb1f | ||
|
|
7da01f7873 |
@@ -88,25 +88,6 @@
|
|||||||
validate-on="input"
|
validate-on="input"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Number Input -->
|
|
||||||
<v-number-input
|
|
||||||
v-else-if="inputField.type === fieldTypes.NUMBER"
|
|
||||||
v-model="model[inputField.varName]"
|
|
||||||
variant="underlined"
|
|
||||||
:control-variant="inputField.numberInputConfig?.controlVariant"
|
|
||||||
density="comfortable"
|
|
||||||
:label="inputField.label"
|
|
||||||
:name="inputField.varName"
|
|
||||||
:min="inputField.numberInputConfig?.min"
|
|
||||||
:max="inputField.numberInputConfig?.max"
|
|
||||||
:precision="inputField.numberInputConfig?.precision"
|
|
||||||
:hint="inputField.hint"
|
|
||||||
:hide-details="!inputField.hint"
|
|
||||||
:persistent-hint="!!inputField.hint"
|
|
||||||
:rules="!(inputField.disableUpdate && updateMode) ? inputField.rules || [] : []"
|
|
||||||
validate-on="input"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Option Select -->
|
<!-- Option Select -->
|
||||||
<v-select
|
<v-select
|
||||||
v-else-if="inputField.type === fieldTypes.SELECT"
|
v-else-if="inputField.type === fieldTypes.SELECT"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
export const fieldTypes = {
|
export const fieldTypes = {
|
||||||
TEXT: "text",
|
TEXT: "text",
|
||||||
TEXT_AREA: "textarea",
|
TEXT_AREA: "textarea",
|
||||||
NUMBER: "number",
|
|
||||||
SELECT: "select",
|
SELECT: "select",
|
||||||
BOOLEAN: "boolean",
|
BOOLEAN: "boolean",
|
||||||
PASSWORD: "password",
|
PASSWORD: "password",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Recipe Actions Data",
|
"recipe-actions-data": "Recipe Actions Data",
|
||||||
"new-recipe-action": "New Recipe Action",
|
"new-recipe-action": "New Recipe Action",
|
||||||
"edit-recipe-action": "Edit Recipe Action",
|
"edit-recipe-action": "Edit Recipe Action",
|
||||||
"action-type": "Action Type"
|
"action-type": "Action Type",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Create Alias",
|
"create-alias": "Create Alias",
|
||||||
"manage-aliases": "Manage Aliases",
|
"manage-aliases": "Manage Aliases",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Recipe Actions Data",
|
"recipe-actions-data": "Recipe Actions Data",
|
||||||
"new-recipe-action": "New Recipe Action",
|
"new-recipe-action": "New Recipe Action",
|
||||||
"edit-recipe-action": "Edit Recipe Action",
|
"edit-recipe-action": "Edit Recipe Action",
|
||||||
"action-type": "Action Type"
|
"action-type": "Action Type",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Create Alias",
|
"create-alias": "Create Alias",
|
||||||
"manage-aliases": "إدارة الأسماء المستعارة",
|
"manage-aliases": "إدارة الأسماء المستعارة",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Данни за действия с рецепти",
|
"recipe-actions-data": "Данни за действия с рецепти",
|
||||||
"new-recipe-action": "Ново действие с рецепта",
|
"new-recipe-action": "Ново действие с рецепта",
|
||||||
"edit-recipe-action": "Редактиране на действието с рецепта",
|
"edit-recipe-action": "Редактиране на действието с рецепта",
|
||||||
"action-type": "Вид действие"
|
"action-type": "Вид действие",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Създаване на псевдоним",
|
"create-alias": "Създаване на псевдоним",
|
||||||
"manage-aliases": "Управление на псевдоними",
|
"manage-aliases": "Управление на псевдоними",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Dades d'accions de receptes",
|
"recipe-actions-data": "Dades d'accions de receptes",
|
||||||
"new-recipe-action": "Nova acció de receptes",
|
"new-recipe-action": "Nova acció de receptes",
|
||||||
"edit-recipe-action": "Edita acció de receptes",
|
"edit-recipe-action": "Edita acció de receptes",
|
||||||
"action-type": "Tipus d'acció"
|
"action-type": "Tipus d'acció",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Crea un àlies",
|
"create-alias": "Crea un àlies",
|
||||||
"manage-aliases": "Gestiona els àlies",
|
"manage-aliases": "Gestiona els àlies",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Data akcí receptu",
|
"recipe-actions-data": "Data akcí receptu",
|
||||||
"new-recipe-action": "Nová akce receptu",
|
"new-recipe-action": "Nová akce receptu",
|
||||||
"edit-recipe-action": "Upravit akci receptu",
|
"edit-recipe-action": "Upravit akci receptu",
|
||||||
"action-type": "Typ akce"
|
"action-type": "Typ akce",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Vytvořit alias",
|
"create-alias": "Vytvořit alias",
|
||||||
"manage-aliases": "Spravovat aliasy",
|
"manage-aliases": "Spravovat aliasy",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Opskriftshandlinger",
|
"recipe-actions-data": "Opskriftshandlinger",
|
||||||
"new-recipe-action": "Ny opskriftshandling",
|
"new-recipe-action": "Ny opskriftshandling",
|
||||||
"edit-recipe-action": "Rediger opskriftshandling",
|
"edit-recipe-action": "Rediger opskriftshandling",
|
||||||
"action-type": "Handlingstype"
|
"action-type": "Handlingstype",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Opret alias",
|
"create-alias": "Opret alias",
|
||||||
"manage-aliases": "Administrer aliaser",
|
"manage-aliases": "Administrer aliaser",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Rezept-Aktionen Daten",
|
"recipe-actions-data": "Rezept-Aktionen Daten",
|
||||||
"new-recipe-action": "Neue Rezept-Aktion",
|
"new-recipe-action": "Neue Rezept-Aktion",
|
||||||
"edit-recipe-action": "Rezept-Aktion bearbeiten",
|
"edit-recipe-action": "Rezept-Aktion bearbeiten",
|
||||||
"action-type": "Aktionstyp"
|
"action-type": "Aktionstyp",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Alias erstellen",
|
"create-alias": "Alias erstellen",
|
||||||
"manage-aliases": "Aliasse verwalten",
|
"manage-aliases": "Aliasse verwalten",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Δεδομένα Ενεργειών Συνταγής",
|
"recipe-actions-data": "Δεδομένα Ενεργειών Συνταγής",
|
||||||
"new-recipe-action": "Νέα Ενέργεια Συνταγής",
|
"new-recipe-action": "Νέα Ενέργεια Συνταγής",
|
||||||
"edit-recipe-action": "Επεξεργασία Ενέργειας Συνταγής",
|
"edit-recipe-action": "Επεξεργασία Ενέργειας Συνταγής",
|
||||||
"action-type": "Τύπος Ενέργειας"
|
"action-type": "Τύπος Ενέργειας",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Δημιουργία ψευδώνυμου",
|
"create-alias": "Δημιουργία ψευδώνυμου",
|
||||||
"manage-aliases": "Διαχείριση ψευδωνύμων",
|
"manage-aliases": "Διαχείριση ψευδωνύμων",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Recipe Actions Data",
|
"recipe-actions-data": "Recipe Actions Data",
|
||||||
"new-recipe-action": "New Recipe Action",
|
"new-recipe-action": "New Recipe Action",
|
||||||
"edit-recipe-action": "Edit Recipe Action",
|
"edit-recipe-action": "Edit Recipe Action",
|
||||||
"action-type": "Action Type"
|
"action-type": "Action Type",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Create Alias",
|
"create-alias": "Create Alias",
|
||||||
"manage-aliases": "Manage Aliases",
|
"manage-aliases": "Manage Aliases",
|
||||||
|
|||||||
@@ -1134,22 +1134,7 @@
|
|||||||
"example-unit-singular": "ex: Tablespoon",
|
"example-unit-singular": "ex: Tablespoon",
|
||||||
"example-unit-plural": "ex: Tablespoons",
|
"example-unit-plural": "ex: Tablespoons",
|
||||||
"example-unit-abbreviation-singular": "ex: Tbsp",
|
"example-unit-abbreviation-singular": "ex: Tbsp",
|
||||||
"example-unit-abbreviation-plural": "ex: Tbsps",
|
"example-unit-abbreviation-plural": "ex: Tbsps"
|
||||||
"standardization": "Standardization",
|
|
||||||
"standardization-description": "How this unit can be represented as a standard unit. This enables unit conversion features such as merging compatible units in shopping lists.",
|
|
||||||
"standard-unit": "Standard Unit",
|
|
||||||
"standard-quantity": "Standard Quantity",
|
|
||||||
"unit-conversion": "Unit Conversion",
|
|
||||||
"standard-unit-labels": {
|
|
||||||
"fluid-ounce": "fluid ounce",
|
|
||||||
"cup": "cup",
|
|
||||||
"ounce": "ounce",
|
|
||||||
"pound": "pound",
|
|
||||||
"milliliter": "milliliter",
|
|
||||||
"liter": "liter",
|
|
||||||
"gram": "gram",
|
|
||||||
"kilogram": "kilogram"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"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.",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Datos de Acciones de Receta",
|
"recipe-actions-data": "Datos de Acciones de Receta",
|
||||||
"new-recipe-action": "Nueva Acción de Receta",
|
"new-recipe-action": "Nueva Acción de Receta",
|
||||||
"edit-recipe-action": "Editar Acción de Receta",
|
"edit-recipe-action": "Editar Acción de Receta",
|
||||||
"action-type": "Tipo de Acción"
|
"action-type": "Tipo de Acción",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Crear un Alias",
|
"create-alias": "Crear un Alias",
|
||||||
"manage-aliases": "Administrar Alias",
|
"manage-aliases": "Administrar Alias",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Retsepti kasutusteave",
|
"recipe-actions-data": "Retsepti kasutusteave",
|
||||||
"new-recipe-action": "Uus retsepti tegevus",
|
"new-recipe-action": "Uus retsepti tegevus",
|
||||||
"edit-recipe-action": "Muuda retsepti tegevust",
|
"edit-recipe-action": "Muuda retsepti tegevust",
|
||||||
"action-type": "Tegevuse tüüp"
|
"action-type": "Tegevuse tüüp",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Loo alias",
|
"create-alias": "Loo alias",
|
||||||
"manage-aliases": "Halda aliaseid",
|
"manage-aliases": "Halda aliaseid",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Reseptin toimintojen käyttötiedot",
|
"recipe-actions-data": "Reseptin toimintojen käyttötiedot",
|
||||||
"new-recipe-action": "Uusi reseptin toiminto",
|
"new-recipe-action": "Uusi reseptin toiminto",
|
||||||
"edit-recipe-action": "Muuta reseptin toimintoa",
|
"edit-recipe-action": "Muuta reseptin toimintoa",
|
||||||
"action-type": "Toiminnon tyyppi"
|
"action-type": "Toiminnon tyyppi",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Luo alias",
|
"create-alias": "Luo alias",
|
||||||
"manage-aliases": "Hallitse aliaksia",
|
"manage-aliases": "Hallitse aliaksia",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Données des actions de recette",
|
"recipe-actions-data": "Données des actions de recette",
|
||||||
"new-recipe-action": "Nouvelle action de recette",
|
"new-recipe-action": "Nouvelle action de recette",
|
||||||
"edit-recipe-action": "Modifier l'action de recette",
|
"edit-recipe-action": "Modifier l'action de recette",
|
||||||
"action-type": "Type d'action"
|
"action-type": "Type d'action",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Créer un alias",
|
"create-alias": "Créer un alias",
|
||||||
"manage-aliases": "Gérer les alias",
|
"manage-aliases": "Gérer les alias",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Données des actions de recette",
|
"recipe-actions-data": "Données des actions de recette",
|
||||||
"new-recipe-action": "Nouvelle action de recette",
|
"new-recipe-action": "Nouvelle action de recette",
|
||||||
"edit-recipe-action": "Modifier l'action de recette",
|
"edit-recipe-action": "Modifier l'action de recette",
|
||||||
"action-type": "Type d'action"
|
"action-type": "Type d'action",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Créer un alias",
|
"create-alias": "Créer un alias",
|
||||||
"manage-aliases": "Gérer les alias",
|
"manage-aliases": "Gérer les alias",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Données des actions de recette",
|
"recipe-actions-data": "Données des actions de recette",
|
||||||
"new-recipe-action": "Nouvelle action de recette",
|
"new-recipe-action": "Nouvelle action de recette",
|
||||||
"edit-recipe-action": "Modifier l'action de recette",
|
"edit-recipe-action": "Modifier l'action de recette",
|
||||||
"action-type": "Type d'action"
|
"action-type": "Type d'action",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Créer un alias",
|
"create-alias": "Créer un alias",
|
||||||
"manage-aliases": "Gérer les alias",
|
"manage-aliases": "Gérer les alias",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Datos das Acións da Receita",
|
"recipe-actions-data": "Datos das Acións da Receita",
|
||||||
"new-recipe-action": "Nova Ación da Receita",
|
"new-recipe-action": "Nova Ación da Receita",
|
||||||
"edit-recipe-action": "Editar Ación da Receita",
|
"edit-recipe-action": "Editar Ación da Receita",
|
||||||
"action-type": "Tipo de Ación"
|
"action-type": "Tipo de Ación",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Crear Pseudónimo",
|
"create-alias": "Crear Pseudónimo",
|
||||||
"manage-aliases": "Xestionar Pseudónimos",
|
"manage-aliases": "Xestionar Pseudónimos",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "מידע על הפעולות במתכון",
|
"recipe-actions-data": "מידע על הפעולות במתכון",
|
||||||
"new-recipe-action": "פעולת-מתכון חדשה",
|
"new-recipe-action": "פעולת-מתכון חדשה",
|
||||||
"edit-recipe-action": "עריכת פעולת-מתכון",
|
"edit-recipe-action": "עריכת פעולת-מתכון",
|
||||||
"action-type": "סוג פעולה"
|
"action-type": "סוג פעולה",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "יצירת שם נרדף",
|
"create-alias": "יצירת שם נרדף",
|
||||||
"manage-aliases": "נהל שמות נרדפים",
|
"manage-aliases": "נהל שמות נרדפים",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Recipe Actions Data",
|
"recipe-actions-data": "Recipe Actions Data",
|
||||||
"new-recipe-action": "New Recipe Action",
|
"new-recipe-action": "New Recipe Action",
|
||||||
"edit-recipe-action": "Edit Recipe Action",
|
"edit-recipe-action": "Edit Recipe Action",
|
||||||
"action-type": "Action Type"
|
"action-type": "Action Type",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Create Alias",
|
"create-alias": "Create Alias",
|
||||||
"manage-aliases": "Manage Aliases",
|
"manage-aliases": "Manage Aliases",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Receptekkel kapcsolatos tevékenységek adatai",
|
"recipe-actions-data": "Receptekkel kapcsolatos tevékenységek adatai",
|
||||||
"new-recipe-action": "Új recept tevékenység",
|
"new-recipe-action": "Új recept tevékenység",
|
||||||
"edit-recipe-action": "Recept tevékenység szerkesztése",
|
"edit-recipe-action": "Recept tevékenység szerkesztése",
|
||||||
"action-type": "Művelet típusa"
|
"action-type": "Művelet típusa",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Alias készítése",
|
"create-alias": "Alias készítése",
|
||||||
"manage-aliases": "Alias kezelése",
|
"manage-aliases": "Alias kezelése",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Gögn fyrir uppskriftaaðgerðir",
|
"recipe-actions-data": "Gögn fyrir uppskriftaaðgerðir",
|
||||||
"new-recipe-action": "Nú uppskriftaaðgerð",
|
"new-recipe-action": "Nú uppskriftaaðgerð",
|
||||||
"edit-recipe-action": "Breyta uppskriftaaðgerð",
|
"edit-recipe-action": "Breyta uppskriftaaðgerð",
|
||||||
"action-type": "Aðgerð"
|
"action-type": "Aðgerð",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Stofna samheiti",
|
"create-alias": "Stofna samheiti",
|
||||||
"manage-aliases": "Vinna með samheiti",
|
"manage-aliases": "Vinna með samheiti",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Dati Azioni Ricetta",
|
"recipe-actions-data": "Dati Azioni Ricetta",
|
||||||
"new-recipe-action": "Nuova Azione Ricetta",
|
"new-recipe-action": "Nuova Azione Ricetta",
|
||||||
"edit-recipe-action": "Modifica Azione Ricetta",
|
"edit-recipe-action": "Modifica Azione Ricetta",
|
||||||
"action-type": "Tipo Di Azione"
|
"action-type": "Tipo Di Azione",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Crea Alias",
|
"create-alias": "Crea Alias",
|
||||||
"manage-aliases": "Gestisci Alias",
|
"manage-aliases": "Gestisci Alias",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "レシピ操作データ",
|
"recipe-actions-data": "レシピ操作データ",
|
||||||
"new-recipe-action": "新しいレシピ操作",
|
"new-recipe-action": "新しいレシピ操作",
|
||||||
"edit-recipe-action": "レシピ操作の編集",
|
"edit-recipe-action": "レシピ操作の編集",
|
||||||
"action-type": "操作タイプ"
|
"action-type": "操作タイプ",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "エイリアスを作成",
|
"create-alias": "エイリアスを作成",
|
||||||
"manage-aliases": "エイリアスの管理",
|
"manage-aliases": "エイリアスの管理",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "레시피 액션 데이터",
|
"recipe-actions-data": "레시피 액션 데이터",
|
||||||
"new-recipe-action": "새 레시피 액션",
|
"new-recipe-action": "새 레시피 액션",
|
||||||
"edit-recipe-action": "레시피 액션 편집",
|
"edit-recipe-action": "레시피 액션 편집",
|
||||||
"action-type": "액션 종류"
|
"action-type": "액션 종류",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "별칭 생성",
|
"create-alias": "별칭 생성",
|
||||||
"manage-aliases": "별칭 관리",
|
"manage-aliases": "별칭 관리",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Recipe Actions Data",
|
"recipe-actions-data": "Recipe Actions Data",
|
||||||
"new-recipe-action": "New Recipe Action",
|
"new-recipe-action": "New Recipe Action",
|
||||||
"edit-recipe-action": "Edit Recipe Action",
|
"edit-recipe-action": "Edit Recipe Action",
|
||||||
"action-type": "Action Type"
|
"action-type": "Action Type",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Create Alias",
|
"create-alias": "Create Alias",
|
||||||
"manage-aliases": "Manage Aliases",
|
"manage-aliases": "Manage Aliases",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Recepšu darbību dati",
|
"recipe-actions-data": "Recepšu darbību dati",
|
||||||
"new-recipe-action": "Jaunas receptes darbība",
|
"new-recipe-action": "Jaunas receptes darbība",
|
||||||
"edit-recipe-action": "Rediģēt receptes darbību",
|
"edit-recipe-action": "Rediģēt receptes darbību",
|
||||||
"action-type": "Darbības veids"
|
"action-type": "Darbības veids",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Izveidot aizstājvārdu",
|
"create-alias": "Izveidot aizstājvārdu",
|
||||||
"manage-aliases": "Pārvaldīt aizstājvārdus",
|
"manage-aliases": "Pārvaldīt aizstājvārdus",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Gegevens voor acties met recepten",
|
"recipe-actions-data": "Gegevens voor acties met recepten",
|
||||||
"new-recipe-action": "Nieuwe actie met recept",
|
"new-recipe-action": "Nieuwe actie met recept",
|
||||||
"edit-recipe-action": "Pas actie met recept aan",
|
"edit-recipe-action": "Pas actie met recept aan",
|
||||||
"action-type": "Soort actie"
|
"action-type": "Soort actie",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Maak alias",
|
"create-alias": "Maak alias",
|
||||||
"manage-aliases": "Beheer aliassen ",
|
"manage-aliases": "Beheer aliassen ",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Opppgavehandlings-data",
|
"recipe-actions-data": "Opppgavehandlings-data",
|
||||||
"new-recipe-action": "Ny oppskriftshandling",
|
"new-recipe-action": "Ny oppskriftshandling",
|
||||||
"edit-recipe-action": "Rediger oppskriftshandling",
|
"edit-recipe-action": "Rediger oppskriftshandling",
|
||||||
"action-type": "Handlingstype"
|
"action-type": "Handlingstype",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Opprett alias",
|
"create-alias": "Opprett alias",
|
||||||
"manage-aliases": "Administrer aliaser",
|
"manage-aliases": "Administrer aliaser",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Dane akcji przepisu",
|
"recipe-actions-data": "Dane akcji przepisu",
|
||||||
"new-recipe-action": "",
|
"new-recipe-action": "",
|
||||||
"edit-recipe-action": "Edycja akcji przepisu",
|
"edit-recipe-action": "Edycja akcji przepisu",
|
||||||
"action-type": "Typ akcji"
|
"action-type": "Typ akcji",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Utwórz alias",
|
"create-alias": "Utwórz alias",
|
||||||
"manage-aliases": "Zarządzaj aliasami",
|
"manage-aliases": "Zarządzaj aliasami",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Dados de Ações de Receita",
|
"recipe-actions-data": "Dados de Ações de Receita",
|
||||||
"new-recipe-action": "Nova Ação de Receita",
|
"new-recipe-action": "Nova Ação de Receita",
|
||||||
"edit-recipe-action": "Alterar Ação de Receita",
|
"edit-recipe-action": "Alterar Ação de Receita",
|
||||||
"action-type": "Tipo de Ação"
|
"action-type": "Tipo de Ação",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Criar Apelido",
|
"create-alias": "Criar Apelido",
|
||||||
"manage-aliases": "Gerenciar apelidos",
|
"manage-aliases": "Gerenciar apelidos",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Dados das Ações da Receita",
|
"recipe-actions-data": "Dados das Ações da Receita",
|
||||||
"new-recipe-action": "Nova Ação da Receita",
|
"new-recipe-action": "Nova Ação da Receita",
|
||||||
"edit-recipe-action": "Editar Ação da Receita",
|
"edit-recipe-action": "Editar Ação da Receita",
|
||||||
"action-type": "Tipo de Ação"
|
"action-type": "Tipo de Ação",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Criar Pseudónimo",
|
"create-alias": "Criar Pseudónimo",
|
||||||
"manage-aliases": "Gerir Pseudónimos",
|
"manage-aliases": "Gerir Pseudónimos",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Date acțiuni rețetă",
|
"recipe-actions-data": "Date acțiuni rețetă",
|
||||||
"new-recipe-action": "Acțiune rețetă nouă",
|
"new-recipe-action": "Acțiune rețetă nouă",
|
||||||
"edit-recipe-action": "Editează Acțiune Rețetă",
|
"edit-recipe-action": "Editează Acțiune Rețetă",
|
||||||
"action-type": "Tip de acțiune"
|
"action-type": "Tip de acțiune",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Creează Alias",
|
"create-alias": "Creează Alias",
|
||||||
"manage-aliases": "Gestionare Aliasuri",
|
"manage-aliases": "Gestionare Aliasuri",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Данные действий рецепта",
|
"recipe-actions-data": "Данные действий рецепта",
|
||||||
"new-recipe-action": "Новое действие с рецептом",
|
"new-recipe-action": "Новое действие с рецептом",
|
||||||
"edit-recipe-action": "Редактировать действие рецепта",
|
"edit-recipe-action": "Редактировать действие рецепта",
|
||||||
"action-type": "Тип Действия"
|
"action-type": "Тип Действия",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Создать псевдоним",
|
"create-alias": "Создать псевдоним",
|
||||||
"manage-aliases": "Управление псевдонимами",
|
"manage-aliases": "Управление псевдонимами",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Dáta akcií receptu",
|
"recipe-actions-data": "Dáta akcií receptu",
|
||||||
"new-recipe-action": "Nová akcia receptu",
|
"new-recipe-action": "Nová akcia receptu",
|
||||||
"edit-recipe-action": "Upraviť akciu receptu",
|
"edit-recipe-action": "Upraviť akciu receptu",
|
||||||
"action-type": "Typ akcie"
|
"action-type": "Typ akcie",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Vytvoriť alias",
|
"create-alias": "Vytvoriť alias",
|
||||||
"manage-aliases": "Spravovať aliasy",
|
"manage-aliases": "Spravovať aliasy",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Podatki o opravilu na receptu",
|
"recipe-actions-data": "Podatki o opravilu na receptu",
|
||||||
"new-recipe-action": "Novo opravilo na receptu",
|
"new-recipe-action": "Novo opravilo na receptu",
|
||||||
"edit-recipe-action": "Urejaj opravilo na receptu",
|
"edit-recipe-action": "Urejaj opravilo na receptu",
|
||||||
"action-type": "Tip opravila"
|
"action-type": "Tip opravila",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Ustvari alias",
|
"create-alias": "Ustvari alias",
|
||||||
"manage-aliases": "Upravljanje z aliasi",
|
"manage-aliases": "Upravljanje z aliasi",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Recipe Actions Data",
|
"recipe-actions-data": "Recipe Actions Data",
|
||||||
"new-recipe-action": "New Recipe Action",
|
"new-recipe-action": "New Recipe Action",
|
||||||
"edit-recipe-action": "Edit Recipe Action",
|
"edit-recipe-action": "Edit Recipe Action",
|
||||||
"action-type": "Action Type"
|
"action-type": "Action Type",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Create Alias",
|
"create-alias": "Create Alias",
|
||||||
"manage-aliases": "Manage Aliases",
|
"manage-aliases": "Manage Aliases",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Data för receptåtgärder",
|
"recipe-actions-data": "Data för receptåtgärder",
|
||||||
"new-recipe-action": "Ny receptåtgärd",
|
"new-recipe-action": "Ny receptåtgärd",
|
||||||
"edit-recipe-action": "Redigera receptåtgärd",
|
"edit-recipe-action": "Redigera receptåtgärd",
|
||||||
"action-type": "Åtgärdstyp"
|
"action-type": "Åtgärdstyp",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Skapa alias",
|
"create-alias": "Skapa alias",
|
||||||
"manage-aliases": "Hantera alias",
|
"manage-aliases": "Hantera alias",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Tarif İşlemleri Verisi",
|
"recipe-actions-data": "Tarif İşlemleri Verisi",
|
||||||
"new-recipe-action": "Yeni Tarif İşlemi",
|
"new-recipe-action": "Yeni Tarif İşlemi",
|
||||||
"edit-recipe-action": "Tarif İşlemini Düzenle",
|
"edit-recipe-action": "Tarif İşlemini Düzenle",
|
||||||
"action-type": "İşlem Türü"
|
"action-type": "İşlem Türü",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Takma Ad Oluştur",
|
"create-alias": "Takma Ad Oluştur",
|
||||||
"manage-aliases": "Takma Adları Yönet",
|
"manage-aliases": "Takma Adları Yönet",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Дані дій рецепта",
|
"recipe-actions-data": "Дані дій рецепта",
|
||||||
"new-recipe-action": "Нова дія рецепту",
|
"new-recipe-action": "Нова дія рецепту",
|
||||||
"edit-recipe-action": "Редагувати дії рецепта",
|
"edit-recipe-action": "Редагувати дії рецепта",
|
||||||
"action-type": "Тип Дії"
|
"action-type": "Тип Дії",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Створити псевдонім",
|
"create-alias": "Створити псевдонім",
|
||||||
"manage-aliases": "Керувати псевдонімами",
|
"manage-aliases": "Керувати псевдонімами",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Recipe Actions Data",
|
"recipe-actions-data": "Recipe Actions Data",
|
||||||
"new-recipe-action": "New Recipe Action",
|
"new-recipe-action": "New Recipe Action",
|
||||||
"edit-recipe-action": "Edit Recipe Action",
|
"edit-recipe-action": "Edit Recipe Action",
|
||||||
"action-type": "Action Type"
|
"action-type": "Action Type",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Create Alias",
|
"create-alias": "Create Alias",
|
||||||
"manage-aliases": "Manage Aliases",
|
"manage-aliases": "Manage Aliases",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "食谱行为数据",
|
"recipe-actions-data": "食谱行为数据",
|
||||||
"new-recipe-action": "新建食谱行为",
|
"new-recipe-action": "新建食谱行为",
|
||||||
"edit-recipe-action": "编辑食谱行为",
|
"edit-recipe-action": "编辑食谱行为",
|
||||||
"action-type": "行为种类"
|
"action-type": "行为种类",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "创建别名",
|
"create-alias": "创建别名",
|
||||||
"manage-aliases": "管理别名",
|
"manage-aliases": "管理别名",
|
||||||
|
|||||||
@@ -1168,7 +1168,11 @@
|
|||||||
"recipe-actions-data": "Recipe Actions Data",
|
"recipe-actions-data": "Recipe Actions Data",
|
||||||
"new-recipe-action": "New Recipe Action",
|
"new-recipe-action": "New Recipe Action",
|
||||||
"edit-recipe-action": "Edit Recipe Action",
|
"edit-recipe-action": "Edit Recipe Action",
|
||||||
"action-type": "Action Type"
|
"action-type": "Action Type",
|
||||||
|
"action-types": {
|
||||||
|
"link": "Link",
|
||||||
|
"post": "Post"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"create-alias": "Create Alias",
|
"create-alias": "Create Alias",
|
||||||
"manage-aliases": "Manage Aliases",
|
"manage-aliases": "Manage Aliases",
|
||||||
|
|||||||
@@ -329,8 +329,6 @@ export interface IngredientUnit {
|
|||||||
pluralAbbreviation?: string | null;
|
pluralAbbreviation?: string | null;
|
||||||
useAbbreviation?: boolean;
|
useAbbreviation?: boolean;
|
||||||
aliases?: IngredientUnitAlias[];
|
aliases?: IngredientUnitAlias[];
|
||||||
standardQuantity?: number | null;
|
|
||||||
standardUnit?: string | null;
|
|
||||||
createdAt?: string | null;
|
createdAt?: string | null;
|
||||||
updatedAt?: string | null;
|
updatedAt?: string | null;
|
||||||
}
|
}
|
||||||
@@ -350,8 +348,6 @@ export interface CreateIngredientUnit {
|
|||||||
pluralAbbreviation?: string | null;
|
pluralAbbreviation?: string | null;
|
||||||
useAbbreviation?: boolean;
|
useAbbreviation?: boolean;
|
||||||
aliases?: CreateIngredientUnitAlias[];
|
aliases?: CreateIngredientUnitAlias[];
|
||||||
standardQuantity?: number | null;
|
|
||||||
standardUnit?: string | null;
|
|
||||||
}
|
}
|
||||||
export interface CreateIngredientUnitAlias {
|
export interface CreateIngredientUnitAlias {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -58,13 +58,3 @@ export interface QueryFilterJSONPart {
|
|||||||
relationalOperator?: RelationalKeyword | RelationalOperator | null;
|
relationalOperator?: RelationalKeyword | RelationalOperator | null;
|
||||||
value?: string | string[] | null;
|
value?: string | string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StandardizedUnitType
|
|
||||||
= | "fluid_ounce"
|
|
||||||
| "cup"
|
|
||||||
| "ounce"
|
|
||||||
| "pound"
|
|
||||||
| "milliliter"
|
|
||||||
| "liter"
|
|
||||||
| "gram"
|
|
||||||
| "kilogram";
|
|
||||||
|
|||||||
@@ -85,8 +85,6 @@ export interface CreateIngredientUnit {
|
|||||||
pluralAbbreviation?: string | null;
|
pluralAbbreviation?: string | null;
|
||||||
useAbbreviation?: boolean;
|
useAbbreviation?: boolean;
|
||||||
aliases?: CreateIngredientUnitAlias[];
|
aliases?: CreateIngredientUnitAlias[];
|
||||||
standardQuantity?: number | null;
|
|
||||||
standardUnit?: string | null;
|
|
||||||
}
|
}
|
||||||
export interface CreateIngredientUnitAlias {
|
export interface CreateIngredientUnitAlias {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -176,8 +174,6 @@ export interface IngredientUnit {
|
|||||||
pluralAbbreviation?: string | null;
|
pluralAbbreviation?: string | null;
|
||||||
useAbbreviation?: boolean;
|
useAbbreviation?: boolean;
|
||||||
aliases?: IngredientUnitAlias[];
|
aliases?: IngredientUnitAlias[];
|
||||||
standardQuantity?: number | null;
|
|
||||||
standardUnit?: string | null;
|
|
||||||
createdAt?: string | null;
|
createdAt?: string | null;
|
||||||
updatedAt?: string | null;
|
updatedAt?: string | null;
|
||||||
}
|
}
|
||||||
@@ -502,8 +498,6 @@ export interface SaveIngredientUnit {
|
|||||||
pluralAbbreviation?: string | null;
|
pluralAbbreviation?: string | null;
|
||||||
useAbbreviation?: boolean;
|
useAbbreviation?: boolean;
|
||||||
aliases?: CreateIngredientUnitAlias[];
|
aliases?: CreateIngredientUnitAlias[];
|
||||||
standardQuantity?: number | null;
|
|
||||||
standardUnit?: string | null;
|
|
||||||
groupId: string;
|
groupId: string;
|
||||||
}
|
}
|
||||||
export interface ScrapeRecipe {
|
export interface ScrapeRecipe {
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ import RecipeDataAliasManagerDialog from "~/components/Domain/Recipe/RecipeDataA
|
|||||||
import { validators } from "~/composables/use-validators";
|
import { validators } from "~/composables/use-validators";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import type { CreateIngredientUnit, IngredientUnit, IngredientUnitAlias } from "~/lib/api/types/recipe";
|
import type { CreateIngredientUnit, IngredientUnit, IngredientUnitAlias } from "~/lib/api/types/recipe";
|
||||||
import type { StandardizedUnitType } from "~/lib/api/types/non-generated";
|
|
||||||
import { useLocales } from "~/composables/use-locales";
|
import { useLocales } from "~/composables/use-locales";
|
||||||
import { normalizeFilter } from "~/composables/use-utils";
|
import { normalizeFilter } from "~/composables/use-utils";
|
||||||
import { useUnitStore } from "~/composables/store";
|
import { useUnitStore } from "~/composables/store";
|
||||||
@@ -220,16 +219,6 @@ const tableHeaders: TableHeaders[] = [
|
|||||||
show: true,
|
show: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: i18n.t("data-pages.units.standard-quantity"),
|
|
||||||
value: "standardQuantity",
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.t("data-pages.units.standard-unit"),
|
|
||||||
value: "standardUnit",
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: i18n.t("general.date-added"),
|
text: i18n.t("general.date-added"),
|
||||||
value: "createdAt",
|
value: "createdAt",
|
||||||
@@ -242,12 +231,7 @@ const { store: unitStore, actions: unitActions } = useUnitStore();
|
|||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Form items (shared)
|
// Form items (shared)
|
||||||
type StandardizedUnitTypeOption = {
|
const formItems: AutoFormItems = [
|
||||||
text: string;
|
|
||||||
value: StandardizedUnitType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formItems = computed<AutoFormItems>(() => [
|
|
||||||
{
|
{
|
||||||
cols: 8,
|
cols: 8,
|
||||||
label: i18n.t("general.name"),
|
label: i18n.t("general.name"),
|
||||||
@@ -278,59 +262,6 @@ const formItems = computed<AutoFormItems>(() => [
|
|||||||
varName: "description",
|
varName: "description",
|
||||||
type: fieldTypes.TEXT,
|
type: fieldTypes.TEXT,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
section: i18n.t("data-pages.units.standardization"),
|
|
||||||
sectionDetails: i18n.t("data-pages.units.standardization-description"),
|
|
||||||
cols: 2,
|
|
||||||
varName: "standardQuantity",
|
|
||||||
type: fieldTypes.NUMBER,
|
|
||||||
numberInputConfig: {
|
|
||||||
min: 0,
|
|
||||||
max: undefined,
|
|
||||||
precision: undefined,
|
|
||||||
controlVariant: "hidden",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cols: 10,
|
|
||||||
varName: "standardUnit",
|
|
||||||
type: fieldTypes.SELECT,
|
|
||||||
selectReturnValue: "value",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
text: i18n.t("data-pages.units.standard-unit-labels.fluid-ounce"),
|
|
||||||
value: "fluid_ounce",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.t("data-pages.units.standard-unit-labels.cup"),
|
|
||||||
value: "cup",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.t("data-pages.units.standard-unit-labels.ounce"),
|
|
||||||
value: "ounce",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.t("data-pages.units.standard-unit-labels.pound"),
|
|
||||||
value: "pound",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.t("data-pages.units.standard-unit-labels.milliliter"),
|
|
||||||
value: "milliliter",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.t("data-pages.units.standard-unit-labels.liter"),
|
|
||||||
value: "liter",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.t("data-pages.units.standard-unit-labels.gram"),
|
|
||||||
value: "gram",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: i18n.t("data-pages.units.standard-unit-labels.kilogram"),
|
|
||||||
value: "kilogram",
|
|
||||||
},
|
|
||||||
] as StandardizedUnitTypeOption[],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
section: i18n.t("general.settings"),
|
section: i18n.t("general.settings"),
|
||||||
cols: 4,
|
cols: 4,
|
||||||
@@ -344,7 +275,7 @@ const formItems = computed<AutoFormItems>(() => [
|
|||||||
varName: "fraction",
|
varName: "fraction",
|
||||||
type: fieldTypes.BOOLEAN,
|
type: fieldTypes.BOOLEAN,
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Create
|
// Create
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
import type { VForm as VuetifyForm } from "vuetify/components/VForm";
|
import type { VForm as VuetifyForm } from "vuetify/components/VForm";
|
||||||
|
|
||||||
type FormFieldType
|
type FormFieldType = "text" | "textarea" | "list" | "select" | "object" | "boolean" | "color" | "password";
|
||||||
= | "text"
|
|
||||||
| "textarea"
|
|
||||||
| "number"
|
|
||||||
| "list"
|
|
||||||
| "select"
|
|
||||||
| "object"
|
|
||||||
| "boolean"
|
|
||||||
| "color"
|
|
||||||
| "password";
|
|
||||||
|
|
||||||
export type FormValidationRule = (value: any) => boolean | string;
|
export type FormValidationRule = (value: any) => boolean | string;
|
||||||
|
|
||||||
@@ -18,13 +9,6 @@ export interface FormSelectOption {
|
|||||||
value?: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FormFieldNumberInputConfig {
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
precision?: number;
|
|
||||||
controlVariant?: "split" | "default" | "hidden" | "stacked";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FormField {
|
export interface FormField {
|
||||||
section?: string;
|
section?: string;
|
||||||
sectionDetails?: string;
|
sectionDetails?: string;
|
||||||
@@ -36,7 +20,6 @@ export interface FormField {
|
|||||||
rules?: FormValidationRule[];
|
rules?: FormValidationRule[];
|
||||||
disableUpdate?: boolean;
|
disableUpdate?: boolean;
|
||||||
disableCreate?: boolean;
|
disableCreate?: boolean;
|
||||||
numberInputConfig?: FormFieldNumberInputConfig;
|
|
||||||
options?: FormSelectOption[];
|
options?: FormSelectOption[];
|
||||||
selectReturnValue?: "text" | "value";
|
selectReturnValue?: "text" | "value";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
"""add unit standardization fields
|
|
||||||
|
|
||||||
Revision ID: a39c7f1826e3
|
|
||||||
Revises: 1d9a002d7234
|
|
||||||
Create Date: 2026-02-21 17:59:01.161812
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy import orm
|
|
||||||
from alembic import op
|
|
||||||
from mealie.repos.repository_units import RepositoryUnit
|
|
||||||
from mealie.core.root_logger import get_logger
|
|
||||||
from mealie.db.models._model_utils.guid import GUID
|
|
||||||
from mealie.repos.seed.seeders import IngredientUnitsSeeder
|
|
||||||
from mealie.lang.locale_config import LOCALE_CONFIG
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "a39c7f1826e3"
|
|
||||||
down_revision: str | None = "1d9a002d7234"
|
|
||||||
branch_labels: str | tuple[str, ...] | None = None
|
|
||||||
depends_on: str | tuple[str, ...] | None = None
|
|
||||||
|
|
||||||
logger = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class SqlAlchemyBase(orm.DeclarativeBase): ...
|
|
||||||
|
|
||||||
|
|
||||||
class IngredientUnitModel(SqlAlchemyBase):
|
|
||||||
__tablename__ = "ingredient_units"
|
|
||||||
|
|
||||||
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
|
|
||||||
name: orm.Mapped[str | None] = orm.mapped_column(sa.String)
|
|
||||||
plural_name: orm.Mapped[str | None] = orm.mapped_column(sa.String)
|
|
||||||
abbreviation: orm.Mapped[str | None] = orm.mapped_column(sa.String)
|
|
||||||
plural_abbreviation: orm.Mapped[str | None] = orm.mapped_column(sa.String)
|
|
||||||
standard_quantity: orm.Mapped[float | None] = orm.mapped_column(sa.Float)
|
|
||||||
standard_unit: orm.Mapped[str | None] = orm.mapped_column(sa.String)
|
|
||||||
|
|
||||||
|
|
||||||
def populate_standards() -> None:
|
|
||||||
bind = op.get_bind()
|
|
||||||
|
|
||||||
session = orm.Session(bind)
|
|
||||||
|
|
||||||
# We aren't using most of the functionality of this class, so we pass dummy args
|
|
||||||
repo = RepositoryUnit(None, None, None, None, group_id=None) # type: ignore
|
|
||||||
|
|
||||||
stmt = sa.select(IngredientUnitModel)
|
|
||||||
units = session.execute(stmt).scalars().all()
|
|
||||||
if not units:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Manually build repo._standardized_unit_map with all locales
|
|
||||||
repo._standardized_unit_map = {}
|
|
||||||
for locale in LOCALE_CONFIG:
|
|
||||||
locale_file = IngredientUnitsSeeder.get_file(locale)
|
|
||||||
for unit_key, unit in IngredientUnitsSeeder.load_file(locale_file).items():
|
|
||||||
for prop in ["name", "plural_name", "abbreviation"]:
|
|
||||||
val = unit.get(prop)
|
|
||||||
if val and isinstance(val, str):
|
|
||||||
repo._standardized_unit_map[val.strip().lower()] = unit_key
|
|
||||||
|
|
||||||
for unit in units:
|
|
||||||
unit_data = {
|
|
||||||
"name": unit.name,
|
|
||||||
"plural_name": unit.plural_name,
|
|
||||||
"abbreviation": unit.abbreviation,
|
|
||||||
"plural_abbreviation": unit.plural_abbreviation,
|
|
||||||
}
|
|
||||||
|
|
||||||
standardized_data = repo._add_standardized_unit(unit_data)
|
|
||||||
std_q = standardized_data.get("standard_quantity")
|
|
||||||
std_u = standardized_data.get("standard_unit")
|
|
||||||
if std_q and std_u:
|
|
||||||
logger.info(f"Found unit '{unit.name}', which is standardized as '{std_q} * {std_u}'")
|
|
||||||
unit.standard_quantity = std_q
|
|
||||||
unit.standard_unit = std_u
|
|
||||||
|
|
||||||
session.commit()
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table("ingredient_units", schema=None) as batch_op:
|
|
||||||
batch_op.add_column(sa.Column("standard_quantity", sa.Float(), nullable=True))
|
|
||||||
batch_op.add_column(sa.Column("standard_unit", sa.String(), nullable=True))
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
# Populate standardized units for existing records
|
|
||||||
try:
|
|
||||||
populate_standards()
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to populate unit standards, skipping...")
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table("ingredient_units", schema=None) as batch_op:
|
|
||||||
batch_op.drop_column("standard_unit")
|
|
||||||
batch_op.drop_column("standard_quantity")
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -52,10 +52,6 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
|||||||
cascade="all, delete, delete-orphan",
|
cascade="all, delete, delete-orphan",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Standardization
|
|
||||||
standard_quantity: Mapped[float | None] = mapped_column(Float)
|
|
||||||
standard_unit: Mapped[str | None] = mapped_column(String)
|
|
||||||
|
|
||||||
# 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)
|
plural_name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||||
|
|||||||
@@ -15,63 +15,52 @@ class LocalePluralFoodHandling(StrEnum):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LocaleConfig:
|
class LocaleConfig:
|
||||||
key: str
|
|
||||||
name: str
|
name: str
|
||||||
dir: LocaleTextDirection = LocaleTextDirection.LTR
|
dir: LocaleTextDirection = LocaleTextDirection.LTR
|
||||||
plural_food_handling: LocalePluralFoodHandling = LocalePluralFoodHandling.ALWAYS
|
plural_food_handling: LocalePluralFoodHandling = LocalePluralFoodHandling.ALWAYS
|
||||||
|
|
||||||
|
|
||||||
LOCALE_CONFIG: dict[str, LocaleConfig] = {
|
LOCALE_CONFIG: dict[str, LocaleConfig] = {
|
||||||
"af-ZA": LocaleConfig(key="af-ZA", name="Afrikaans (Afrikaans)"),
|
"af-ZA": LocaleConfig(name="Afrikaans (Afrikaans)"),
|
||||||
"ar-SA": LocaleConfig(key="ar-SA", name="العربية (Arabic)", dir=LocaleTextDirection.RTL),
|
"ar-SA": LocaleConfig(name="العربية (Arabic)", dir=LocaleTextDirection.RTL),
|
||||||
"bg-BG": LocaleConfig(key="bg-BG", name="Български (Bulgarian)"),
|
"bg-BG": LocaleConfig(name="Български (Bulgarian)"),
|
||||||
"ca-ES": LocaleConfig(key="ca-ES", name="Català (Catalan)"),
|
"ca-ES": LocaleConfig(name="Català (Catalan)"),
|
||||||
"cs-CZ": LocaleConfig(key="cs-CZ", name="Čeština (Czech)"),
|
"cs-CZ": LocaleConfig(name="Čeština (Czech)"),
|
||||||
"da-DK": LocaleConfig(key="da-DK", name="Dansk (Danish)"),
|
"da-DK": LocaleConfig(name="Dansk (Danish)"),
|
||||||
"de-DE": LocaleConfig(key="de-DE", name="Deutsch (German)"),
|
"de-DE": LocaleConfig(name="Deutsch (German)"),
|
||||||
"el-GR": LocaleConfig(key="el-GR", name="Ελληνικά (Greek)"),
|
"el-GR": LocaleConfig(name="Ελληνικά (Greek)"),
|
||||||
"en-GB": LocaleConfig(
|
"en-GB": LocaleConfig(name="British English", plural_food_handling=LocalePluralFoodHandling.WITHOUT_UNIT),
|
||||||
key="en-GB", name="British English", plural_food_handling=LocalePluralFoodHandling.WITHOUT_UNIT
|
"en-US": LocaleConfig(name="American English", plural_food_handling=LocalePluralFoodHandling.WITHOUT_UNIT),
|
||||||
),
|
"es-ES": LocaleConfig(name="Español (Spanish)"),
|
||||||
"en-US": LocaleConfig(
|
"et-EE": LocaleConfig(name="Eesti (Estonian)"),
|
||||||
key="en-US", name="American English", plural_food_handling=LocalePluralFoodHandling.WITHOUT_UNIT
|
"fi-FI": LocaleConfig(name="Suomi (Finnish)"),
|
||||||
),
|
"fr-BE": LocaleConfig(name="Belge (Belgian)"),
|
||||||
"es-ES": LocaleConfig(key="es-ES", name="Español (Spanish)"),
|
"fr-CA": LocaleConfig(name="Français canadien (Canadian French)"),
|
||||||
"et-EE": LocaleConfig(key="et-EE", name="Eesti (Estonian)"),
|
"fr-FR": LocaleConfig(name="Français (French)"),
|
||||||
"fi-FI": LocaleConfig(key="fi-FI", name="Suomi (Finnish)"),
|
"gl-ES": LocaleConfig(name="Galego (Galician)"),
|
||||||
"fr-BE": LocaleConfig(key="fr-BE", name="Belge (Belgian)"),
|
"he-IL": LocaleConfig(name="עברית (Hebrew)", dir=LocaleTextDirection.RTL),
|
||||||
"fr-CA": LocaleConfig(key="fr-CA", name="Français canadien (Canadian French)"),
|
"hr-HR": LocaleConfig(name="Hrvatski (Croatian)"),
|
||||||
"fr-FR": LocaleConfig(key="fr-FR", name="Français (French)"),
|
"hu-HU": LocaleConfig(name="Magyar (Hungarian)"),
|
||||||
"gl-ES": LocaleConfig(key="gl-ES", name="Galego (Galician)"),
|
"is-IS": LocaleConfig(name="Íslenska (Icelandic)"),
|
||||||
"he-IL": LocaleConfig(key="he-IL", name="עברית (Hebrew)", dir=LocaleTextDirection.RTL),
|
"it-IT": LocaleConfig(name="Italiano (Italian)"),
|
||||||
"hr-HR": LocaleConfig(key="hr-HR", name="Hrvatski (Croatian)"),
|
"ja-JP": LocaleConfig(name="日本語 (Japanese)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||||
"hu-HU": LocaleConfig(key="hu-HU", name="Magyar (Hungarian)"),
|
"ko-KR": LocaleConfig(name="한국어 (Korean)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||||
"is-IS": LocaleConfig(key="is-IS", name="Íslenska (Icelandic)"),
|
"lt-LT": LocaleConfig(name="Lietuvių (Lithuanian)"),
|
||||||
"it-IT": LocaleConfig(key="it-IT", name="Italiano (Italian)"),
|
"lv-LV": LocaleConfig(name="Latviešu (Latvian)"),
|
||||||
"ja-JP": LocaleConfig(key="ja-JP", name="日本語 (Japanese)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
"nl-NL": LocaleConfig(name="Nederlands (Dutch)"),
|
||||||
"ko-KR": LocaleConfig(key="ko-KR", name="한국어 (Korean)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
"no-NO": LocaleConfig(name="Norsk (Norwegian)"),
|
||||||
"lt-LT": LocaleConfig(key="lt-LT", name="Lietuvių (Lithuanian)"),
|
"pl-PL": LocaleConfig(name="Polski (Polish)"),
|
||||||
"lv-LV": LocaleConfig(key="lv-LV", name="Latviešu (Latvian)"),
|
"pt-BR": LocaleConfig(name="Português do Brasil (Brazilian Portuguese)"),
|
||||||
"nl-NL": LocaleConfig(key="nl-NL", name="Nederlands (Dutch)"),
|
"pt-PT": LocaleConfig(name="Português (Portuguese)"),
|
||||||
"no-NO": LocaleConfig(key="no-NO", name="Norsk (Norwegian)"),
|
"ro-RO": LocaleConfig(name="Română (Romanian)"),
|
||||||
"pl-PL": LocaleConfig(key="pl-PL", name="Polski (Polish)"),
|
"ru-RU": LocaleConfig(name="Pусский (Russian)"),
|
||||||
"pt-BR": LocaleConfig(key="pt-BR", name="Português do Brasil (Brazilian Portuguese)"),
|
"sk-SK": LocaleConfig(name="Slovenčina (Slovak)"),
|
||||||
"pt-PT": LocaleConfig(key="pt-PT", name="Português (Portuguese)"),
|
"sl-SI": LocaleConfig(name="Slovenščina (Slovenian)"),
|
||||||
"ro-RO": LocaleConfig(key="ro-RO", name="Română (Romanian)"),
|
"sr-SP": LocaleConfig(name="српски (Serbian)"),
|
||||||
"ru-RU": LocaleConfig(key="ru-RU", name="Pусский (Russian)"),
|
"sv-SE": LocaleConfig(name="Svenska (Swedish)"),
|
||||||
"sk-SK": LocaleConfig(key="sk-SK", name="Slovenčina (Slovak)"),
|
"tr-TR": LocaleConfig(name="Türkçe (Turkish)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||||
"sl-SI": LocaleConfig(key="sl-SI", name="Slovenščina (Slovenian)"),
|
"uk-UA": LocaleConfig(name="Українська (Ukrainian)"),
|
||||||
"sr-SP": LocaleConfig(key="sr-SP", name="српски (Serbian)"),
|
"vi-VN": LocaleConfig(name="Tiếng Việt (Vietnamese)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||||
"sv-SE": LocaleConfig(key="sv-SE", name="Svenska (Swedish)"),
|
"zh-CN": LocaleConfig(name="简体中文 (Chinese simplified)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||||
"tr-TR": LocaleConfig(key="tr-TR", name="Türkçe (Turkish)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
"zh-TW": LocaleConfig(name="繁體中文 (Chinese traditional)", plural_food_handling=LocalePluralFoodHandling.NEVER),
|
||||||
"uk-UA": LocaleConfig(key="uk-UA", name="Українська (Ukrainian)"),
|
|
||||||
"vi-VN": LocaleConfig(
|
|
||||||
key="vi-VN", name="Tiếng Việt (Vietnamese)", plural_food_handling=LocalePluralFoodHandling.NEVER
|
|
||||||
),
|
|
||||||
"zh-CN": LocaleConfig(
|
|
||||||
key="zh-CN", name="简体中文 (Chinese simplified)", plural_food_handling=LocalePluralFoodHandling.NEVER
|
|
||||||
),
|
|
||||||
"zh-TW": LocaleConfig(
|
|
||||||
key="zh-TW", name="繁體中文 (Chinese traditional)", plural_food_handling=LocalePluralFoodHandling.NEVER
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,119 +1,17 @@
|
|||||||
from collections.abc import Iterable
|
from pydantic import UUID4
|
||||||
|
|
||||||
from pydantic import UUID4, BaseModel
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
from mealie.db.models.recipe.ingredient import IngredientUnitModel
|
from mealie.db.models.recipe.ingredient import IngredientUnitModel
|
||||||
from mealie.lang.providers import get_locale_context
|
from mealie.schema.recipe.recipe_ingredient import IngredientUnit
|
||||||
from mealie.schema.recipe.recipe_ingredient import IngredientUnit, StandardizedUnitType
|
|
||||||
|
|
||||||
from .repository_generic import GroupRepositoryGeneric
|
from .repository_generic import GroupRepositoryGeneric
|
||||||
|
|
||||||
|
|
||||||
class RepositoryUnit(GroupRepositoryGeneric[IngredientUnit, IngredientUnitModel]):
|
class RepositoryUnit(GroupRepositoryGeneric[IngredientUnit, IngredientUnitModel]):
|
||||||
_standardized_unit_map: dict[str, str] | None = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def standardized_unit_map(self) -> dict[str, str]:
|
|
||||||
"""A map of potential known units to its standardized name in our seed data"""
|
|
||||||
|
|
||||||
if self._standardized_unit_map is None:
|
|
||||||
from .seed.seeders import IngredientUnitsSeeder
|
|
||||||
|
|
||||||
ctx = get_locale_context()
|
|
||||||
if ctx:
|
|
||||||
locale = ctx[1].key
|
|
||||||
else:
|
|
||||||
locale = None
|
|
||||||
|
|
||||||
self._standardized_unit_map = {}
|
|
||||||
locale_file = IngredientUnitsSeeder.get_file(locale=locale)
|
|
||||||
for unit_key, unit in IngredientUnitsSeeder.load_file(locale_file).items():
|
|
||||||
for prop in ["name", "plural_name", "abbreviation"]:
|
|
||||||
val = unit.get(prop)
|
|
||||||
if val and isinstance(val, str):
|
|
||||||
self._standardized_unit_map[val.strip().lower()] = unit_key
|
|
||||||
|
|
||||||
return self._standardized_unit_map
|
|
||||||
|
|
||||||
def _get_unit(self, id: UUID4) -> IngredientUnitModel:
|
def _get_unit(self, id: UUID4) -> IngredientUnitModel:
|
||||||
stmt = select(self.model).filter_by(**self._filter_builder(**{"id": id}))
|
stmt = select(self.model).filter_by(**self._filter_builder(**{"id": id}))
|
||||||
return self.session.execute(stmt).scalars().one()
|
return self.session.execute(stmt).scalars().one()
|
||||||
|
|
||||||
def _add_standardized_unit(self, data: BaseModel | dict) -> dict:
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
data = data.model_dump()
|
|
||||||
|
|
||||||
# Don't overwrite user data if it exists
|
|
||||||
if data.get("standard_quantity") is not None or data.get("standard_unit") is not None:
|
|
||||||
return data
|
|
||||||
|
|
||||||
# Compare name attrs to translation files and see if there's a match to a known standard unit
|
|
||||||
for prop in ["name", "plural_name", "abbreviation", "plural_abbreviation"]:
|
|
||||||
val = data.get(prop)
|
|
||||||
if not (val and isinstance(val, str)):
|
|
||||||
continue
|
|
||||||
|
|
||||||
standardized_unit_key = self.standardized_unit_map.get(val.strip().lower())
|
|
||||||
if not standardized_unit_key:
|
|
||||||
continue
|
|
||||||
|
|
||||||
match standardized_unit_key:
|
|
||||||
case "teaspoon":
|
|
||||||
data["standard_quantity"] = 1 / 6
|
|
||||||
data["standard_unit"] = StandardizedUnitType.FLUID_OUNCE
|
|
||||||
case "tablespoon":
|
|
||||||
data["standard_quantity"] = 1 / 2
|
|
||||||
data["standard_unit"] = StandardizedUnitType.FLUID_OUNCE
|
|
||||||
case "cup":
|
|
||||||
data["standard_quantity"] = 1
|
|
||||||
data["standard_unit"] = StandardizedUnitType.CUP
|
|
||||||
case "fluid-ounce":
|
|
||||||
data["standard_quantity"] = 1
|
|
||||||
data["standard_unit"] = StandardizedUnitType.FLUID_OUNCE
|
|
||||||
case "pint":
|
|
||||||
data["standard_quantity"] = 2
|
|
||||||
data["standard_unit"] = StandardizedUnitType.CUP
|
|
||||||
case "quart":
|
|
||||||
data["standard_quantity"] = 4
|
|
||||||
data["standard_unit"] = StandardizedUnitType.CUP
|
|
||||||
case "gallon":
|
|
||||||
data["standard_quantity"] = 16
|
|
||||||
data["standard_unit"] = StandardizedUnitType.CUP
|
|
||||||
case "milliliter":
|
|
||||||
data["standard_quantity"] = 1
|
|
||||||
data["standard_unit"] = StandardizedUnitType.MILLILITER
|
|
||||||
case "liter":
|
|
||||||
data["standard_quantity"] = 1
|
|
||||||
data["standard_unit"] = StandardizedUnitType.LITER
|
|
||||||
case "pound":
|
|
||||||
data["standard_quantity"] = 1
|
|
||||||
data["standard_unit"] = StandardizedUnitType.POUND
|
|
||||||
case "ounce":
|
|
||||||
data["standard_quantity"] = 1
|
|
||||||
data["standard_unit"] = StandardizedUnitType.OUNCE
|
|
||||||
case "gram":
|
|
||||||
data["standard_quantity"] = 1
|
|
||||||
data["standard_unit"] = StandardizedUnitType.GRAM
|
|
||||||
case "kilogram":
|
|
||||||
data["standard_quantity"] = 1
|
|
||||||
data["standard_unit"] = StandardizedUnitType.KILOGRAM
|
|
||||||
case "milligram":
|
|
||||||
data["standard_quantity"] = 1 / 1000
|
|
||||||
data["standard_unit"] = StandardizedUnitType.GRAM
|
|
||||||
case _:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def create(self, data: IngredientUnit | dict) -> IngredientUnit:
|
|
||||||
data = self._add_standardized_unit(data)
|
|
||||||
return super().create(data)
|
|
||||||
|
|
||||||
def create_many(self, data: Iterable[IngredientUnit | dict]) -> list[IngredientUnit]:
|
|
||||||
data = [self._add_standardized_unit(i) for i in data]
|
|
||||||
return super().create_many(data)
|
|
||||||
|
|
||||||
def merge(self, from_unit: UUID4, to_unit: UUID4) -> IngredientUnit | None:
|
def merge(self, from_unit: UUID4, to_unit: UUID4) -> IngredientUnit | None:
|
||||||
from_model = self._get_unit(from_unit)
|
from_model = self._get_unit(from_unit)
|
||||||
to_model = self._get_unit(to_unit)
|
to_model = self._get_unit(to_unit)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -12,8 +11,6 @@ class AbstractSeeder(ABC):
|
|||||||
Abstract class for seeding data.
|
Abstract class for seeding data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
resources = Path(__file__).parent / "resources"
|
|
||||||
|
|
||||||
def __init__(self, db: AllRepositories, logger: Logger | None = None):
|
def __init__(self, db: AllRepositories, logger: Logger | None = None):
|
||||||
"""
|
"""
|
||||||
Initialize the abstract seeder.
|
Initialize the abstract seeder.
|
||||||
@@ -22,14 +19,7 @@ class AbstractSeeder(ABC):
|
|||||||
"""
|
"""
|
||||||
self.repos = db
|
self.repos = db
|
||||||
self.logger = logger or get_logger("Data Seeder")
|
self.logger = logger or get_logger("Data Seeder")
|
||||||
|
self.resources = Path(__file__).parent / "resources"
|
||||||
@classmethod
|
|
||||||
@abstractmethod
|
|
||||||
def get_file(self, locale: str | None = None) -> Path: ...
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def load_file(self, file: Path) -> dict[str, dict]:
|
|
||||||
return json.loads(file.read_text(encoding="utf-8"))
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def seed(self, locale: str | None = None) -> None: ...
|
def seed(self, locale: str | None = None) -> None: ...
|
||||||
|
|||||||
@@ -5377,8 +5377,8 @@
|
|||||||
"shark meat": {
|
"shark meat": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "shark meat",
|
"name": "haaienvlees",
|
||||||
"plural_name": "shark meats"
|
"plural_name": "haaienvlees"
|
||||||
},
|
},
|
||||||
"garoupa": {
|
"garoupa": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
@@ -5687,7 +5687,7 @@
|
|||||||
"salted shrimp": {
|
"salted shrimp": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "salted shrimp",
|
"name": "gezouten garnalen",
|
||||||
"plural_name": "gezouten garnalen"
|
"plural_name": "gezouten garnalen"
|
||||||
},
|
},
|
||||||
"yaki-nori": {
|
"yaki-nori": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "Landbrugsprodukt"
|
"name": "Landbrugsprodukter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Korn"
|
"name": "Korn"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
@@ -20,10 +21,9 @@ class MultiPurposeLabelSeeder(AbstractSeeder):
|
|||||||
def service(self):
|
def service(self):
|
||||||
return MultiPurposeLabelService(self.repos)
|
return MultiPurposeLabelService(self.repos)
|
||||||
|
|
||||||
@classmethod
|
def get_file(self, locale: str | None = None) -> pathlib.Path:
|
||||||
def get_file(cls, locale: str | None = None) -> pathlib.Path:
|
|
||||||
# Get the labels from the foods seed file now
|
# Get the labels from the foods seed file now
|
||||||
locale_path = cls.resources / "foods" / "locales" / f"{locale}.json"
|
locale_path = self.resources / "foods" / "locales" / f"{locale}.json"
|
||||||
return locale_path if locale_path.exists() else foods.en_US
|
return locale_path if locale_path.exists() else foods.en_US
|
||||||
|
|
||||||
def get_all_labels(self) -> list[MultiPurposeLabelOut]:
|
def get_all_labels(self) -> list[MultiPurposeLabelOut]:
|
||||||
@@ -34,7 +34,7 @@ class MultiPurposeLabelSeeder(AbstractSeeder):
|
|||||||
|
|
||||||
current_label_names = {label.name for label in self.get_all_labels()}
|
current_label_names = {label.name for label in self.get_all_labels()}
|
||||||
# load from the foods locale file and remove any empty strings
|
# load from the foods locale file and remove any empty strings
|
||||||
seed_label_names = set(filter(None, self.load_file(file).keys())) # type: set[str]
|
seed_label_names = set(filter(None, json.loads(file.read_text(encoding="utf-8")).keys())) # type: set[str]
|
||||||
# only seed new labels
|
# only seed new labels
|
||||||
to_seed_labels = seed_label_names - current_label_names
|
to_seed_labels = seed_label_names - current_label_names
|
||||||
for label in to_seed_labels:
|
for label in to_seed_labels:
|
||||||
@@ -53,9 +53,8 @@ class MultiPurposeLabelSeeder(AbstractSeeder):
|
|||||||
|
|
||||||
|
|
||||||
class IngredientUnitsSeeder(AbstractSeeder):
|
class IngredientUnitsSeeder(AbstractSeeder):
|
||||||
@classmethod
|
def get_file(self, locale: str | None = None) -> pathlib.Path:
|
||||||
def get_file(cls, locale: str | None = None) -> pathlib.Path:
|
locale_path = self.resources / "units" / "locales" / f"{locale}.json"
|
||||||
locale_path = cls.resources / "units" / "locales" / f"{locale}.json"
|
|
||||||
return locale_path if locale_path.exists() else units.en_US
|
return locale_path if locale_path.exists() else units.en_US
|
||||||
|
|
||||||
def get_all_units(self) -> list[IngredientUnit]:
|
def get_all_units(self) -> list[IngredientUnit]:
|
||||||
@@ -65,7 +64,7 @@ class IngredientUnitsSeeder(AbstractSeeder):
|
|||||||
file = self.get_file(locale)
|
file = self.get_file(locale)
|
||||||
|
|
||||||
seen_unit_names = {unit.name for unit in self.get_all_units()}
|
seen_unit_names = {unit.name for unit in self.get_all_units()}
|
||||||
for unit in self.load_file(file).values():
|
for unit in json.loads(file.read_text(encoding="utf-8")).values():
|
||||||
if unit["name"] in seen_unit_names:
|
if unit["name"] in seen_unit_names:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -89,9 +88,8 @@ class IngredientUnitsSeeder(AbstractSeeder):
|
|||||||
|
|
||||||
|
|
||||||
class IngredientFoodsSeeder(AbstractSeeder):
|
class IngredientFoodsSeeder(AbstractSeeder):
|
||||||
@classmethod
|
def get_file(self, locale: str | None = None) -> pathlib.Path:
|
||||||
def get_file(cls, locale: str | None = None) -> pathlib.Path:
|
locale_path = self.resources / "foods" / "locales" / f"{locale}.json"
|
||||||
locale_path = cls.resources / "foods" / "locales" / f"{locale}.json"
|
|
||||||
return locale_path if locale_path.exists() else foods.en_US
|
return locale_path if locale_path.exists() else foods.en_US
|
||||||
|
|
||||||
def get_label(self, value: str) -> MultiPurposeLabelOut | None:
|
def get_label(self, value: str) -> MultiPurposeLabelOut | None:
|
||||||
@@ -105,7 +103,7 @@ class IngredientFoodsSeeder(AbstractSeeder):
|
|||||||
|
|
||||||
# get all current unique foods
|
# get all current unique foods
|
||||||
seen_foods_names = {food.name for food in self.get_all_foods()}
|
seen_foods_names = {food.name for food in self.get_all_foods()}
|
||||||
for label, values in self.load_file(file).items():
|
for label, values in json.loads(file.read_text(encoding="utf-8")).items():
|
||||||
label_out = self.get_label(label)
|
label_out = self.get_label(label)
|
||||||
|
|
||||||
for food_name, attributes in values["foods"].items():
|
for food_name, attributes in values["foods"].items():
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ from .recipe_ingredient import (
|
|||||||
RegisteredParser,
|
RegisteredParser,
|
||||||
SaveIngredientFood,
|
SaveIngredientFood,
|
||||||
SaveIngredientUnit,
|
SaveIngredientUnit,
|
||||||
StandardizedUnitType,
|
|
||||||
UnitFoodBase,
|
UnitFoodBase,
|
||||||
)
|
)
|
||||||
from .recipe_notes import RecipeNote
|
from .recipe_notes import RecipeNote
|
||||||
@@ -160,7 +159,6 @@ __all__ = [
|
|||||||
"RegisteredParser",
|
"RegisteredParser",
|
||||||
"SaveIngredientFood",
|
"SaveIngredientFood",
|
||||||
"SaveIngredientUnit",
|
"SaveIngredientUnit",
|
||||||
"StandardizedUnitType",
|
|
||||||
"UnitFoodBase",
|
"UnitFoodBase",
|
||||||
"RecipeSuggestionQuery",
|
"RecipeSuggestionQuery",
|
||||||
"RecipeSuggestionResponse",
|
"RecipeSuggestionResponse",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import enum
|
import enum
|
||||||
from enum import StrEnum
|
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
@@ -35,28 +34,6 @@ def display_fraction(fraction: Fraction):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class StandardizedUnitType(StrEnum):
|
|
||||||
"""
|
|
||||||
An arbitrary list of standardized units supported by unit conversions.
|
|
||||||
The backend doesn't really care what standardized unit you use, as long as it's recognized,
|
|
||||||
but defining them here keeps it consistant with the frontend.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Imperial
|
|
||||||
FLUID_OUNCE = "fluid_ounce"
|
|
||||||
CUP = "cup"
|
|
||||||
|
|
||||||
OUNCE = "ounce"
|
|
||||||
POUND = "pound"
|
|
||||||
|
|
||||||
# Metric
|
|
||||||
MILLILITER = "milliliter"
|
|
||||||
LITER = "liter"
|
|
||||||
|
|
||||||
GRAM = "gram"
|
|
||||||
KILOGRAM = "kilogram"
|
|
||||||
|
|
||||||
|
|
||||||
class UnitFoodBase(MealieModel):
|
class UnitFoodBase(MealieModel):
|
||||||
id: UUID4 | None = None
|
id: UUID4 | None = None
|
||||||
name: str
|
name: str
|
||||||
@@ -132,6 +109,9 @@ class IngredientFood(CreateIngredientFood):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
def is_on_hand(self, household_slug: str) -> bool:
|
||||||
|
return household_slug in self.households_with_tool
|
||||||
|
|
||||||
|
|
||||||
class IngredientFoodPagination(PaginationBase):
|
class IngredientFoodPagination(PaginationBase):
|
||||||
items: list[IngredientFood]
|
items: list[IngredientFood]
|
||||||
@@ -150,21 +130,7 @@ class CreateIngredientUnit(UnitFoodBase):
|
|||||||
abbreviation: str = ""
|
abbreviation: str = ""
|
||||||
plural_abbreviation: str | None = ""
|
plural_abbreviation: str | None = ""
|
||||||
use_abbreviation: bool = False
|
use_abbreviation: bool = False
|
||||||
|
|
||||||
aliases: list[CreateIngredientUnitAlias] = []
|
aliases: list[CreateIngredientUnitAlias] = []
|
||||||
standard_quantity: float | None = None
|
|
||||||
standard_unit: str | None = None
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
|
||||||
def validate_standardization_fields(self):
|
|
||||||
# If one is set, the other must be set.
|
|
||||||
# If quantity is <= 0, it's considered not set.
|
|
||||||
if not self.standard_unit:
|
|
||||||
self.standard_quantity = self.standard_unit = None
|
|
||||||
elif not ((self.standard_quantity or 0) > 0):
|
|
||||||
self.standard_quantity = self.standard_unit = None
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class SaveIngredientUnit(CreateIngredientUnit):
|
class SaveIngredientUnit(CreateIngredientUnit):
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ class RecipeToolOut(RecipeToolCreate):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
def is_on_hand(self, household_slug: str) -> bool:
|
||||||
|
return household_slug in self.households_with_tool
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def loader_options(cls) -> list[LoaderOption]:
|
def loader_options(cls) -> list[LoaderOption]:
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ from mealie.schema.recipe.recipe_ingredient import (
|
|||||||
)
|
)
|
||||||
from mealie.schema.response.pagination import OrderDirection, PaginationQuery
|
from mealie.schema.response.pagination import OrderDirection, PaginationQuery
|
||||||
from mealie.services.parser_services._base import DataMatcher
|
from mealie.services.parser_services._base import DataMatcher
|
||||||
from mealie.services.parser_services.parser_utils import UnitConverter, merge_quantity_and_unit
|
|
||||||
|
|
||||||
|
|
||||||
class ShoppingListService:
|
class ShoppingListService:
|
||||||
@@ -42,7 +41,8 @@ class ShoppingListService:
|
|||||||
self.list_refs = repos.group_shopping_list_recipe_refs
|
self.list_refs = repos.group_shopping_list_recipe_refs
|
||||||
self.data_matcher = DataMatcher(self.repos, food_fuzzy_match_threshold=self.DEFAULT_FOOD_FUZZY_MATCH_THRESHOLD)
|
self.data_matcher = DataMatcher(self.repos, food_fuzzy_match_threshold=self.DEFAULT_FOOD_FUZZY_MATCH_THRESHOLD)
|
||||||
|
|
||||||
def can_merge(self, item1: ShoppingListItemBase, item2: ShoppingListItemBase) -> bool:
|
@staticmethod
|
||||||
|
def can_merge(item1: ShoppingListItemBase, item2: ShoppingListItemBase) -> bool:
|
||||||
"""Check to see if this item can be merged with another item"""
|
"""Check to see if this item can be merged with another item"""
|
||||||
|
|
||||||
if any(
|
if any(
|
||||||
@@ -50,28 +50,16 @@ class ShoppingListService:
|
|||||||
item1.checked,
|
item1.checked,
|
||||||
item2.checked,
|
item2.checked,
|
||||||
item1.food_id != item2.food_id,
|
item1.food_id != item2.food_id,
|
||||||
|
item1.unit_id != item2.unit_id,
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# check if units match or if they're compatable
|
|
||||||
if item1.unit_id != item2.unit_id:
|
|
||||||
item1_unit = item1.unit or self.data_matcher.units_by_id.get(item1.unit_id)
|
|
||||||
item2_unit = item2.unit or self.data_matcher.units_by_id.get(item2.unit_id)
|
|
||||||
if not (item1_unit and item1_unit.standard_unit):
|
|
||||||
return False
|
|
||||||
if not (item2_unit and item2_unit.standard_unit):
|
|
||||||
return False
|
|
||||||
|
|
||||||
uc = UnitConverter()
|
|
||||||
if not uc.can_convert(item1_unit.standard_unit, item2_unit.standard_unit):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# if foods match, we can merge, otherwise compare the notes
|
# if foods match, we can merge, otherwise compare the notes
|
||||||
return bool(item1.food_id) or item1.note == item2.note
|
return bool(item1.food_id) or item1.note == item2.note
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def merge_items(
|
def merge_items(
|
||||||
self,
|
|
||||||
from_item: ShoppingListItemCreate | ShoppingListItemUpdateBulk,
|
from_item: ShoppingListItemCreate | ShoppingListItemUpdateBulk,
|
||||||
to_item: ShoppingListItemCreate | ShoppingListItemUpdateBulk | ShoppingListItemOut,
|
to_item: ShoppingListItemCreate | ShoppingListItemUpdateBulk | ShoppingListItemOut,
|
||||||
) -> ShoppingListItemUpdate:
|
) -> ShoppingListItemUpdate:
|
||||||
@@ -81,20 +69,7 @@ class ShoppingListService:
|
|||||||
Attributes of the `to_item` take priority over the `from_item`, except extras with overlapping keys
|
Attributes of the `to_item` take priority over the `from_item`, except extras with overlapping keys
|
||||||
"""
|
"""
|
||||||
|
|
||||||
to_item_unit = to_item.unit or self.data_matcher.units_by_id.get(to_item.unit_id)
|
|
||||||
from_item_unit = from_item.unit or self.data_matcher.units_by_id.get(from_item.unit_id)
|
|
||||||
if to_item_unit and to_item_unit.standard_unit and from_item_unit and from_item_unit.standard_unit:
|
|
||||||
merged_qty, merged_unit = merge_quantity_and_unit(
|
|
||||||
from_item.quantity or 0, from_item_unit, to_item.quantity or 0, to_item_unit
|
|
||||||
)
|
|
||||||
to_item.quantity = merged_qty
|
|
||||||
to_item.unit_id = merged_unit.id
|
|
||||||
to_item.unit = merged_unit
|
|
||||||
|
|
||||||
else:
|
|
||||||
# No conversion needed, just sum the quantities
|
|
||||||
to_item.quantity += from_item.quantity
|
to_item.quantity += from_item.quantity
|
||||||
|
|
||||||
if to_item.note != from_item.note:
|
if to_item.note != from_item.note:
|
||||||
to_item.note = " | ".join([note for note in [to_item.note, from_item.note] if note])
|
to_item.note = " | ".join([note for note in [to_item.note, from_item.note] if note])
|
||||||
|
|
||||||
|
|||||||
@@ -28,38 +28,18 @@ class DataMatcher:
|
|||||||
|
|
||||||
self._food_fuzzy_match_threshold = food_fuzzy_match_threshold
|
self._food_fuzzy_match_threshold = food_fuzzy_match_threshold
|
||||||
self._unit_fuzzy_match_threshold = unit_fuzzy_match_threshold
|
self._unit_fuzzy_match_threshold = unit_fuzzy_match_threshold
|
||||||
|
|
||||||
self._foods_by_id: dict[UUID4, IngredientFood] | None = None
|
|
||||||
self._units_by_id: dict[UUID4, IngredientUnit] | None = None
|
|
||||||
|
|
||||||
self._foods_by_alias: dict[str, IngredientFood] | None = None
|
self._foods_by_alias: dict[str, IngredientFood] | None = None
|
||||||
self._units_by_alias: dict[str, IngredientUnit] | None = None
|
self._units_by_alias: dict[str, IngredientUnit] | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def foods_by_id(self) -> dict[UUID4, IngredientFood]:
|
def foods_by_alias(self) -> dict[str, IngredientFood]:
|
||||||
if self._foods_by_id is None:
|
if self._foods_by_alias is None:
|
||||||
foods_repo = self.repos.ingredient_foods
|
foods_repo = self.repos.ingredient_foods
|
||||||
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_id = {food.id: food for food in all_foods}
|
|
||||||
|
|
||||||
return self._foods_by_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def units_by_id(self) -> dict[UUID4, IngredientUnit]:
|
|
||||||
if self._units_by_id is None:
|
|
||||||
units_repo = self.repos.ingredient_units
|
|
||||||
query = PaginationQuery(page=1, per_page=-1)
|
|
||||||
all_units = units_repo.page_all(query).items
|
|
||||||
self._units_by_id = {unit.id: unit for unit in all_units}
|
|
||||||
|
|
||||||
return self._units_by_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def foods_by_alias(self) -> dict[str, IngredientFood]:
|
|
||||||
if self._foods_by_alias is None:
|
|
||||||
foods_by_alias: dict[str, IngredientFood] = {}
|
foods_by_alias: dict[str, IngredientFood] = {}
|
||||||
for food in self.foods_by_id.values():
|
for food in all_foods:
|
||||||
if food.name:
|
if food.name:
|
||||||
foods_by_alias[IngredientFoodModel.normalize(food.name)] = food
|
foods_by_alias[IngredientFoodModel.normalize(food.name)] = food
|
||||||
if food.plural_name:
|
if food.plural_name:
|
||||||
@@ -76,8 +56,12 @@ class DataMatcher:
|
|||||||
@property
|
@property
|
||||||
def units_by_alias(self) -> dict[str, IngredientUnit]:
|
def units_by_alias(self) -> dict[str, IngredientUnit]:
|
||||||
if self._units_by_alias is None:
|
if self._units_by_alias is None:
|
||||||
|
units_repo = self.repos.ingredient_units
|
||||||
|
query = PaginationQuery(page=1, per_page=-1)
|
||||||
|
all_units = units_repo.page_all(query).items
|
||||||
|
|
||||||
units_by_alias: dict[str, IngredientUnit] = {}
|
units_by_alias: dict[str, IngredientUnit] = {}
|
||||||
for unit in self.units_by_id.values():
|
for unit in all_units:
|
||||||
if unit.name:
|
if unit.name:
|
||||||
units_by_alias[IngredientUnitModel.normalize(unit.name)] = unit
|
units_by_alias[IngredientUnitModel.normalize(unit.name)] = unit
|
||||||
if unit.plural_name:
|
if unit.plural_name:
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
from .string_utils import *
|
from .string_utils import *
|
||||||
from .unit_utils import *
|
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
from typing import TYPE_CHECKING, Literal, overload
|
|
||||||
|
|
||||||
from pint import Quantity, Unit, UnitRegistry
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit
|
|
||||||
|
|
||||||
|
|
||||||
class UnitNotFound(Exception):
|
|
||||||
"""Raised when trying to access a unit not found in the unit registry."""
|
|
||||||
|
|
||||||
def __init__(self, message: str = "Unit not found in unit registry"):
|
|
||||||
self.message = message
|
|
||||||
super().__init__(self.message)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.message}"
|
|
||||||
|
|
||||||
|
|
||||||
class UnitConverter:
|
|
||||||
def __init__(self):
|
|
||||||
self.ureg = UnitRegistry()
|
|
||||||
|
|
||||||
def _resolve_ounce(self, unit_1: Unit, unit_2: Unit) -> tuple[Unit, Unit]:
|
|
||||||
"""
|
|
||||||
Often times "ounce" is used in place of "fluid ounce" in recipes.
|
|
||||||
When trying to convert/combine ounces with a volume, we can assume it should have been a fluid ounce.
|
|
||||||
This function will convert ounces to fluid ounces if the other unit is a volume.
|
|
||||||
"""
|
|
||||||
|
|
||||||
OUNCE = self.ureg("ounce")
|
|
||||||
FL_OUNCE = self.ureg("fluid_ounce")
|
|
||||||
VOLUME = "[length] ** 3"
|
|
||||||
|
|
||||||
if unit_1 == OUNCE and unit_2.dimensionality == VOLUME:
|
|
||||||
return FL_OUNCE, unit_2
|
|
||||||
if unit_2 == OUNCE and unit_1.dimensionality == VOLUME:
|
|
||||||
return unit_1, FL_OUNCE
|
|
||||||
|
|
||||||
return unit_1, unit_2
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def parse(self, unit: str | Unit, strict: Literal[False] = False) -> str | Unit: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def parse(self, unit: str | Unit, strict: Literal[True]) -> Unit: ...
|
|
||||||
|
|
||||||
def parse(self, unit: str | Unit, strict: bool = False) -> str | Unit:
|
|
||||||
"""
|
|
||||||
Parse a string unit into a pint.Unit.
|
|
||||||
|
|
||||||
If strict is False (default), returns a pint.Unit if it exists, otherwise returns the original string.
|
|
||||||
If strict is True, raises UnitNotFound instead of returning a string.
|
|
||||||
If the input is already a parsed pint.Unit, returns it as-is.
|
|
||||||
"""
|
|
||||||
if isinstance(unit, Unit):
|
|
||||||
return unit
|
|
||||||
|
|
||||||
try:
|
|
||||||
return self.ureg(unit).units
|
|
||||||
except Exception as e:
|
|
||||||
if strict:
|
|
||||||
raise UnitNotFound(f"Unit '{unit}' not found in unit registry") from e
|
|
||||||
return unit
|
|
||||||
|
|
||||||
def can_convert(self, unit: str | Unit, to_unit: str | Unit) -> bool:
|
|
||||||
"""Whether or not a given unit can be converted into another unit."""
|
|
||||||
|
|
||||||
unit = self.parse(unit)
|
|
||||||
to_unit = self.parse(to_unit)
|
|
||||||
|
|
||||||
if not (isinstance(unit, Unit) and isinstance(to_unit, Unit)):
|
|
||||||
return False
|
|
||||||
|
|
||||||
unit, to_unit = self._resolve_ounce(unit, to_unit)
|
|
||||||
return unit.is_compatible_with(to_unit)
|
|
||||||
|
|
||||||
def convert(self, quantity: float, unit: str | Unit, to_unit: str | Unit) -> tuple[float, Unit]:
|
|
||||||
"""
|
|
||||||
Convert a quantity and a unit into another unit.
|
|
||||||
|
|
||||||
Returns tuple[quantity, unit]
|
|
||||||
"""
|
|
||||||
|
|
||||||
unit = self.parse(unit, strict=True)
|
|
||||||
to_unit = self.parse(to_unit, strict=True)
|
|
||||||
unit, to_unit = self._resolve_ounce(unit, to_unit)
|
|
||||||
|
|
||||||
qty = quantity * unit
|
|
||||||
converted = qty.to(to_unit)
|
|
||||||
return float(converted.magnitude), converted.units
|
|
||||||
|
|
||||||
def merge(self, quantity_1: float, unit_1: str | Unit, quantity_2: float, unit_2: str | Unit) -> tuple[float, Unit]:
|
|
||||||
"""Merge two quantities together"""
|
|
||||||
|
|
||||||
unit_1 = self.parse(unit_1, strict=True)
|
|
||||||
unit_2 = self.parse(unit_2, strict=True)
|
|
||||||
unit_1, unit_2 = self._resolve_ounce(unit_1, unit_2)
|
|
||||||
|
|
||||||
q1 = quantity_1 * unit_1
|
|
||||||
q2 = quantity_2 * unit_2
|
|
||||||
|
|
||||||
out: Quantity = q1 + q2
|
|
||||||
return float(out.magnitude), out.units
|
|
||||||
|
|
||||||
|
|
||||||
def merge_quantity_and_unit[T: CreateIngredientUnit](
|
|
||||||
qty_1: float, unit_1: T, qty_2: float, unit_2: T
|
|
||||||
) -> tuple[float, T]:
|
|
||||||
"""
|
|
||||||
Merge a quantity and unit.
|
|
||||||
|
|
||||||
Returns tuple[quantity, unit]
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not (unit_1.standard_quantity and unit_1.standard_unit and unit_2.standard_quantity and unit_2.standard_unit):
|
|
||||||
raise ValueError("Both units must contain standardized unit data")
|
|
||||||
|
|
||||||
PINT_UNIT_1_TXT = "_mealie_unit_1"
|
|
||||||
PINT_UNIT_2_TXT = "_mealie_unit_2"
|
|
||||||
|
|
||||||
uc = UnitConverter()
|
|
||||||
|
|
||||||
# pre-process units to account for ounce -> fluid_ounce conversion
|
|
||||||
unit_1_standard = uc.parse(unit_1.standard_unit, strict=True)
|
|
||||||
unit_2_standard = uc.parse(unit_2.standard_unit, strict=True)
|
|
||||||
unit_1_standard, unit_2_standard = uc._resolve_ounce(unit_1_standard, unit_2_standard)
|
|
||||||
|
|
||||||
# create custon unit definition so pint can handle them natively
|
|
||||||
uc.ureg.define(f"{PINT_UNIT_1_TXT} = {unit_1.standard_quantity} * {unit_1_standard}")
|
|
||||||
uc.ureg.define(f"{PINT_UNIT_2_TXT} = {unit_2.standard_quantity} * {unit_2_standard}")
|
|
||||||
|
|
||||||
pint_unit_1 = uc.parse(PINT_UNIT_1_TXT)
|
|
||||||
pint_unit_2 = uc.parse(PINT_UNIT_2_TXT)
|
|
||||||
|
|
||||||
merged_q, merged_u = uc.merge(qty_1, pint_unit_1, qty_2, pint_unit_2)
|
|
||||||
|
|
||||||
# Convert to the bigger unit if quantity >= 1, else the smaller unit
|
|
||||||
merged_q, merged_u = uc.convert(merged_q, merged_u, max(pint_unit_1, pint_unit_2))
|
|
||||||
if abs(merged_q) < 1:
|
|
||||||
merged_q, merged_u = uc.convert(merged_q, merged_u, min(pint_unit_1, pint_unit_2))
|
|
||||||
|
|
||||||
if str(merged_u) == PINT_UNIT_1_TXT:
|
|
||||||
return merged_q, unit_1
|
|
||||||
else:
|
|
||||||
return merged_q, unit_2
|
|
||||||
@@ -46,7 +46,6 @@ dependencies = [
|
|||||||
"typing-extensions==4.15.0",
|
"typing-extensions==4.15.0",
|
||||||
"itsdangerous==2.2.0",
|
"itsdangerous==2.2.0",
|
||||||
"ingredient-parser-nlp==2.5.0",
|
"ingredient-parser-nlp==2.5.0",
|
||||||
"pint>=0.25",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
@@ -71,7 +70,7 @@ dev = [
|
|||||||
"pytest==9.0.2",
|
"pytest==9.0.2",
|
||||||
"pytest-asyncio==1.3.0",
|
"pytest-asyncio==1.3.0",
|
||||||
"rich==14.3.3",
|
"rich==14.3.3",
|
||||||
"ruff==0.15.2",
|
"ruff==0.15.3",
|
||||||
"types-PyYAML==6.0.12.20250915",
|
"types-PyYAML==6.0.12.20250915",
|
||||||
"types-python-dateutil==2.9.0.20260124",
|
"types-python-dateutil==2.9.0.20260124",
|
||||||
"types-python-slugify==8.0.2.20240310",
|
"types-python-slugify==8.0.2.20240310",
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ CWD = Path(__file__).parent
|
|||||||
|
|
||||||
locale_dir = CWD / "locale"
|
locale_dir = CWD / "locale"
|
||||||
|
|
||||||
backup_version_1d9a002d7234_1 = CWD / "backups/backup-version-1d9a002d7234-1.zip"
|
|
||||||
"""1d9a002d7234: add referenced_recipe to ingredients"""
|
|
||||||
|
|
||||||
backup_version_44e8d670719d_1 = CWD / "backups/backup-version-44e8d670719d-1.zip"
|
backup_version_44e8d670719d_1 = CWD / "backups/backup-version-44e8d670719d-1.zip"
|
||||||
"""44e8d670719d: add extras to shopping lists, list items, and ingredient foods"""
|
"""44e8d670719d: add extras to shopping lists, list items, and ingredient foods"""
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -15,12 +15,14 @@ def test_seed_foods(api_client: TestClient, unique_user: TestUser):
|
|||||||
CREATED_FOODS = 2687
|
CREATED_FOODS = 2687
|
||||||
database = unique_user.repos
|
database = unique_user.repos
|
||||||
|
|
||||||
|
# Check that the foods was created
|
||||||
foods = database.ingredient_foods.page_all(PaginationQuery(page=1, per_page=-1)).items
|
foods = database.ingredient_foods.page_all(PaginationQuery(page=1, per_page=-1)).items
|
||||||
assert len(foods) == 0
|
assert len(foods) == 0
|
||||||
|
|
||||||
resp = api_client.post(api_routes.groups_seeders_foods, json={"locale": "en-US"}, headers=unique_user.token)
|
resp = api_client.post(api_routes.groups_seeders_foods, json={"locale": "en-US"}, headers=unique_user.token)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# Check that the foods was created
|
||||||
foods = database.ingredient_foods.page_all(PaginationQuery(page=1, per_page=-1)).items
|
foods = database.ingredient_foods.page_all(PaginationQuery(page=1, per_page=-1)).items
|
||||||
assert len(foods) == CREATED_FOODS
|
assert len(foods) == CREATED_FOODS
|
||||||
|
|
||||||
@@ -29,37 +31,29 @@ def test_seed_units(api_client: TestClient, unique_user: TestUser):
|
|||||||
CREATED_UNITS = 24
|
CREATED_UNITS = 24
|
||||||
database = unique_user.repos
|
database = unique_user.repos
|
||||||
|
|
||||||
|
# Check that the foods was created
|
||||||
units = database.ingredient_units.page_all(PaginationQuery(page=1, per_page=-1)).items
|
units = database.ingredient_units.page_all(PaginationQuery(page=1, per_page=-1)).items
|
||||||
assert len(units) == 0
|
assert len(units) == 0
|
||||||
|
|
||||||
resp = api_client.post(api_routes.groups_seeders_units, json={"locale": "en-US"}, headers=unique_user.token)
|
resp = api_client.post(api_routes.groups_seeders_units, json={"locale": "en-US"}, headers=unique_user.token)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# Check that the foods was created
|
||||||
units = database.ingredient_units.page_all(PaginationQuery(page=1, per_page=-1)).items
|
units = database.ingredient_units.page_all(PaginationQuery(page=1, per_page=-1)).items
|
||||||
assert len(units) == CREATED_UNITS
|
assert len(units) == CREATED_UNITS
|
||||||
|
|
||||||
# Check that the "pint" unit was created and includes standardized data
|
|
||||||
pint_found = False
|
|
||||||
for unit in units:
|
|
||||||
if unit.name != "pint":
|
|
||||||
continue
|
|
||||||
|
|
||||||
pint_found = True
|
|
||||||
assert unit.standard_quantity == 2
|
|
||||||
assert unit.standard_unit == "cup"
|
|
||||||
|
|
||||||
assert pint_found
|
|
||||||
|
|
||||||
|
|
||||||
def test_seed_labels(api_client: TestClient, unique_user: TestUser):
|
def test_seed_labels(api_client: TestClient, unique_user: TestUser):
|
||||||
CREATED_LABELS = 32
|
CREATED_LABELS = 32
|
||||||
database = unique_user.repos
|
database = unique_user.repos
|
||||||
|
|
||||||
|
# Check that the foods was created
|
||||||
labels = database.group_multi_purpose_labels.page_all(PaginationQuery(page=1, per_page=-1)).items
|
labels = database.group_multi_purpose_labels.page_all(PaginationQuery(page=1, per_page=-1)).items
|
||||||
assert len(labels) == 0
|
assert len(labels) == 0
|
||||||
|
|
||||||
resp = api_client.post(api_routes.groups_seeders_labels, json={"locale": "en-US"}, headers=unique_user.token)
|
resp = api_client.post(api_routes.groups_seeders_labels, json={"locale": "en-US"}, headers=unique_user.token)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# Check that the foods was created
|
||||||
labels = database.group_multi_purpose_labels.page_all(PaginationQuery(page=1, per_page=-1)).items
|
labels = database.group_multi_purpose_labels.page_all(PaginationQuery(page=1, per_page=-1)).items
|
||||||
assert len(labels) == CREATED_LABELS
|
assert len(labels) == CREATED_LABELS
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from fastapi.testclient import TestClient
|
|||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.schema.household.group_shopping_list import ShoppingListItemOut, ShoppingListOut
|
from mealie.schema.household.group_shopping_list import ShoppingListItemOut, ShoppingListOut
|
||||||
from mealie.schema.recipe.recipe_ingredient import IngredientUnit, SaveIngredientFood
|
from mealie.schema.recipe.recipe_ingredient import SaveIngredientFood
|
||||||
from tests import utils
|
from tests import utils
|
||||||
from tests.utils import api_routes
|
from tests.utils import api_routes
|
||||||
from tests.utils.factories import random_int, random_string
|
from tests.utils.factories import random_int, random_string
|
||||||
@@ -641,96 +641,6 @@ def test_shopping_list_items_with_zero_quantity(
|
|||||||
assert len(as_json["listItems"]) == len(normal_items + zero_qty_items) - 1
|
assert len(as_json["listItems"]) == len(normal_items + zero_qty_items) - 1
|
||||||
|
|
||||||
|
|
||||||
def test_shopping_list_merge_standard_unit(
|
|
||||||
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
|
|
||||||
):
|
|
||||||
unit_1_cup_data = {"name": random_string(), "standardQuantity": 1, "standardUnit": "cup"}
|
|
||||||
unit_2_cup_data = {"name": random_string(), "standardQuantity": 2, "standardUnit": "cup"}
|
|
||||||
unit_1_out = api_client.post(api_routes.units, json=unit_1_cup_data, headers=unique_user.token)
|
|
||||||
unit_2_out = api_client.post(api_routes.units, json=unit_2_cup_data, headers=unique_user.token)
|
|
||||||
|
|
||||||
unit_1 = IngredientUnit.model_validate(unit_1_out.json())
|
|
||||||
unit_2 = IngredientUnit.model_validate(unit_2_out.json())
|
|
||||||
|
|
||||||
list_item_1_data = create_item(shopping_list.id, unit_id=str(unit_1.id), note="mealie-food")
|
|
||||||
list_item_2_data = create_item(shopping_list.id, unit_id=str(unit_2.id), note="mealie-food")
|
|
||||||
response = api_client.post(
|
|
||||||
api_routes.households_shopping_items_create_bulk,
|
|
||||||
json=[list_item_1_data, list_item_2_data],
|
|
||||||
headers=unique_user.token,
|
|
||||||
)
|
|
||||||
|
|
||||||
as_json = utils.assert_deserialize(response, 201)
|
|
||||||
assert len(as_json["createdItems"]) == 1
|
|
||||||
|
|
||||||
item_out = as_json["createdItems"][0]
|
|
||||||
|
|
||||||
# should use larger "2 cup" unit (a la "pint")
|
|
||||||
assert item_out["unitId"] == str(unit_2.id)
|
|
||||||
# calculate quantity by summing base "cup" amount and dividing by 2 (a la pints)
|
|
||||||
assert item_out["quantity"] == (list_item_1_data["quantity"] + (list_item_2_data["quantity"] * 2)) / 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_shopping_list_merge_standard_unit_different_foods(
|
|
||||||
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
|
|
||||||
):
|
|
||||||
unit_1_cup_data = {"name": random_string(), "standardQuantity": 1, "standardUnit": "cup"}
|
|
||||||
unit_2_cup_data = {"name": random_string(), "standardQuantity": 2, "standardUnit": "cup"}
|
|
||||||
unit_1_out = api_client.post(api_routes.units, json=unit_1_cup_data, headers=unique_user.token)
|
|
||||||
unit_2_out = api_client.post(api_routes.units, json=unit_2_cup_data, headers=unique_user.token)
|
|
||||||
|
|
||||||
unit_1 = IngredientUnit.model_validate(unit_1_out.json())
|
|
||||||
unit_2 = IngredientUnit.model_validate(unit_2_out.json())
|
|
||||||
|
|
||||||
list_item_1_data = create_item(shopping_list.id, unit_id=str(unit_1.id), note="mealie-food-1")
|
|
||||||
list_item_2_data = create_item(shopping_list.id, unit_id=str(unit_2.id), note="mealie-food-2")
|
|
||||||
response = api_client.post(
|
|
||||||
api_routes.households_shopping_items_create_bulk,
|
|
||||||
json=[list_item_1_data, list_item_2_data],
|
|
||||||
headers=unique_user.token,
|
|
||||||
)
|
|
||||||
|
|
||||||
as_json = utils.assert_deserialize(response, 201)
|
|
||||||
assert len(as_json["createdItems"]) == 2
|
|
||||||
for in_data, out_data in zip(
|
|
||||||
[list_item_1_data, list_item_2_data], [as_json["createdItems"][0], as_json["createdItems"][1]], strict=True
|
|
||||||
):
|
|
||||||
assert in_data["quantity"] == out_data["quantity"]
|
|
||||||
assert out_data["unit"]
|
|
||||||
assert in_data["unit_id"] == out_data["unit"]["id"]
|
|
||||||
assert in_data["note"] == out_data["note"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_shopping_list_merge_standard_unit_incompatible_units(
|
|
||||||
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
|
|
||||||
):
|
|
||||||
unit_1_data = {"name": random_string(), "standardQuantity": 1, "standardUnit": "cup"}
|
|
||||||
unit_2_data = {"name": random_string(), "standardQuantity": 2, "standardUnit": "gram"}
|
|
||||||
unit_1_out = api_client.post(api_routes.units, json=unit_1_data, headers=unique_user.token)
|
|
||||||
unit_2_out = api_client.post(api_routes.units, json=unit_2_data, headers=unique_user.token)
|
|
||||||
|
|
||||||
unit_1 = IngredientUnit.model_validate(unit_1_out.json())
|
|
||||||
unit_2 = IngredientUnit.model_validate(unit_2_out.json())
|
|
||||||
|
|
||||||
list_item_1_data = create_item(shopping_list.id, unit_id=str(unit_1.id), note="mealie-food")
|
|
||||||
list_item_2_data = create_item(shopping_list.id, unit_id=str(unit_2.id), note="mealie-food")
|
|
||||||
response = api_client.post(
|
|
||||||
api_routes.households_shopping_items_create_bulk,
|
|
||||||
json=[list_item_1_data, list_item_2_data],
|
|
||||||
headers=unique_user.token,
|
|
||||||
)
|
|
||||||
|
|
||||||
as_json = utils.assert_deserialize(response, 201)
|
|
||||||
assert len(as_json["createdItems"]) == 2
|
|
||||||
for in_data, out_data in zip(
|
|
||||||
[list_item_1_data, list_item_2_data], [as_json["createdItems"][0], as_json["createdItems"][1]], strict=True
|
|
||||||
):
|
|
||||||
assert in_data["quantity"] == out_data["quantity"]
|
|
||||||
assert out_data["unit"]
|
|
||||||
assert in_data["unit_id"] == out_data["unit"]["id"]
|
|
||||||
assert in_data["note"] == out_data["note"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_shopping_list_item_extras(
|
def test_shopping_list_item_extras(
|
||||||
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
|
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import pytest
|
|||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from mealie.schema.recipe.recipe_ingredient import RegisteredParser
|
from mealie.schema.recipe.recipe_ingredient import RegisteredParser
|
||||||
from tests.unit_tests.ingredient_parser.test_ingredient_parser import TestIngredient
|
from tests.unit_tests.test_ingredient_parser import TestIngredient
|
||||||
from tests.utils import api_routes
|
from tests.utils import api_routes
|
||||||
from tests.utils.fixture_schemas import TestUser
|
from tests.utils.fixture_schemas import TestUser
|
||||||
|
|
||||||
|
|||||||
@@ -1,309 +0,0 @@
|
|||||||
import pint
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit
|
|
||||||
from mealie.services.parser_services.parser_utils import UnitConverter, UnitNotFound, merge_quantity_and_unit
|
|
||||||
from tests.utils import random_string
|
|
||||||
|
|
||||||
|
|
||||||
def test_uc_parse_string():
|
|
||||||
uc = UnitConverter()
|
|
||||||
parsed = uc.parse("cup")
|
|
||||||
|
|
||||||
assert isinstance(parsed, pint.Unit)
|
|
||||||
assert (str(parsed)) == "cup"
|
|
||||||
|
|
||||||
|
|
||||||
def test_uc_parse_unit():
|
|
||||||
uc = UnitConverter()
|
|
||||||
parsed = uc.parse(uc.parse("cup"))
|
|
||||||
|
|
||||||
assert isinstance(parsed, pint.Unit)
|
|
||||||
assert (str(parsed)) == "cup"
|
|
||||||
|
|
||||||
|
|
||||||
def test_uc_parse_invalid():
|
|
||||||
uc = UnitConverter()
|
|
||||||
input_str = random_string()
|
|
||||||
parsed = uc.parse(input_str)
|
|
||||||
|
|
||||||
assert not isinstance(parsed, pint.Unit)
|
|
||||||
assert parsed == input_str
|
|
||||||
|
|
||||||
|
|
||||||
def test_uc_parse_invalid_strict():
|
|
||||||
uc = UnitConverter()
|
|
||||||
input_str = random_string()
|
|
||||||
|
|
||||||
with pytest.raises(UnitNotFound):
|
|
||||||
uc.parse(input_str, strict=True)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("pre_parse_1", [True, False])
|
|
||||||
@pytest.mark.parametrize("pre_parse_2", [True, False])
|
|
||||||
def test_can_convert(pre_parse_1: bool, pre_parse_2: bool):
|
|
||||||
unit_1 = "cup"
|
|
||||||
unit_2 = "pint"
|
|
||||||
|
|
||||||
uc = UnitConverter()
|
|
||||||
if pre_parse_1:
|
|
||||||
unit_1 = uc.parse(unit_1)
|
|
||||||
if pre_parse_2:
|
|
||||||
unit_2 = uc.parse(unit_2)
|
|
||||||
|
|
||||||
assert uc.can_convert(unit_1, unit_2)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("pre_parse_1", [True, False])
|
|
||||||
@pytest.mark.parametrize("pre_parse_2", [True, False])
|
|
||||||
def test_cannot_convert(pre_parse_1: bool, pre_parse_2: bool):
|
|
||||||
unit_1 = "cup"
|
|
||||||
unit_2 = "pound"
|
|
||||||
|
|
||||||
uc = UnitConverter()
|
|
||||||
if pre_parse_1:
|
|
||||||
unit_1 = uc.parse(unit_1)
|
|
||||||
if pre_parse_2:
|
|
||||||
unit_2 = uc.parse(unit_2)
|
|
||||||
|
|
||||||
assert not uc.can_convert(unit_1, unit_2)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cannot_convert_invalid_unit():
|
|
||||||
uc = UnitConverter()
|
|
||||||
assert not uc.can_convert("cup", random_string())
|
|
||||||
assert not uc.can_convert(random_string(), "cup")
|
|
||||||
|
|
||||||
|
|
||||||
def test_can_convert_same_unit():
|
|
||||||
uc = UnitConverter()
|
|
||||||
assert uc.can_convert("cup", "cup")
|
|
||||||
|
|
||||||
|
|
||||||
def test_can_convert_volume_ounce():
|
|
||||||
uc = UnitConverter()
|
|
||||||
assert uc.can_convert("ounce", "cup")
|
|
||||||
assert uc.can_convert("cup", "ounce")
|
|
||||||
|
|
||||||
|
|
||||||
def test_convert_simple():
|
|
||||||
uc = UnitConverter()
|
|
||||||
quantity, unit = uc.convert(1, "cup", "pint")
|
|
||||||
|
|
||||||
assert isinstance(unit, pint.Unit)
|
|
||||||
assert str(unit) == "pint"
|
|
||||||
assert quantity == 1 / 2
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("pre_parse_1", [True, False])
|
|
||||||
@pytest.mark.parametrize("pre_parse_2", [True, False])
|
|
||||||
def test_convert_pre_parsed(pre_parse_1: bool, pre_parse_2: bool):
|
|
||||||
unit_1 = "cup"
|
|
||||||
unit_2 = "pint"
|
|
||||||
|
|
||||||
uc = UnitConverter()
|
|
||||||
if pre_parse_1:
|
|
||||||
unit_1 = uc.parse(unit_1)
|
|
||||||
if pre_parse_2:
|
|
||||||
unit_2 = uc.parse(unit_2)
|
|
||||||
|
|
||||||
quantity, unit = uc.convert(1, unit_1, unit_2)
|
|
||||||
assert isinstance(unit, pint.Unit)
|
|
||||||
assert str(unit) == "pint"
|
|
||||||
assert quantity == 1 / 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_convert_weight():
|
|
||||||
uc = UnitConverter()
|
|
||||||
quantity, unit = uc.convert(16, "ounce", "pound")
|
|
||||||
|
|
||||||
assert isinstance(unit, pint.Unit)
|
|
||||||
assert str(unit) == "pound"
|
|
||||||
assert quantity == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_convert_zero_quantity():
|
|
||||||
uc = UnitConverter()
|
|
||||||
quantity, unit = uc.convert(0, "cup", "pint")
|
|
||||||
|
|
||||||
assert isinstance(unit, pint.Unit)
|
|
||||||
assert quantity == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_convert_invalid_unit():
|
|
||||||
uc = UnitConverter()
|
|
||||||
|
|
||||||
with pytest.raises(UnitNotFound):
|
|
||||||
uc.convert(1, "pound", random_string())
|
|
||||||
|
|
||||||
|
|
||||||
def test_convert_incompatible_units():
|
|
||||||
uc = UnitConverter()
|
|
||||||
|
|
||||||
with pytest.raises(pint.errors.DimensionalityError):
|
|
||||||
uc.convert(1, "pound", "cup")
|
|
||||||
|
|
||||||
|
|
||||||
def test_convert_volume_ounce():
|
|
||||||
uc = UnitConverter()
|
|
||||||
quantity, unit = uc.convert(8, "ounce", "cup")
|
|
||||||
|
|
||||||
assert isinstance(unit, pint.Unit)
|
|
||||||
assert str(unit) == "cup"
|
|
||||||
assert quantity == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_same_unit():
|
|
||||||
uc = UnitConverter()
|
|
||||||
quantity, unit = uc.merge(1, "cup", 2, "cup")
|
|
||||||
|
|
||||||
assert isinstance(unit, pint.Unit)
|
|
||||||
assert str(unit) == "cup"
|
|
||||||
assert quantity == 3
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("pre_parse_1", [True, False])
|
|
||||||
@pytest.mark.parametrize("pre_parse_2", [True, False])
|
|
||||||
def test_merge_compatible_units(pre_parse_1: bool, pre_parse_2: bool):
|
|
||||||
unit_1 = "cup"
|
|
||||||
unit_2 = "pint"
|
|
||||||
|
|
||||||
uc = UnitConverter()
|
|
||||||
if pre_parse_1:
|
|
||||||
unit_1 = uc.parse(unit_1)
|
|
||||||
if pre_parse_2:
|
|
||||||
unit_2 = uc.parse(unit_2)
|
|
||||||
|
|
||||||
quantity, unit = uc.merge(1, unit_1, 1, unit_2)
|
|
||||||
assert isinstance(unit, pint.Unit)
|
|
||||||
# 1 cup + 1 pint = 1 cup + 2 cups = 3 cups
|
|
||||||
assert quantity == 3
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_weight_units():
|
|
||||||
uc = UnitConverter()
|
|
||||||
quantity, unit = uc.merge(8, "ounce", 8, "ounce")
|
|
||||||
|
|
||||||
assert isinstance(unit, pint.Unit)
|
|
||||||
assert str(unit) == "ounce"
|
|
||||||
assert quantity == 16
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_different_weight_units():
|
|
||||||
uc = UnitConverter()
|
|
||||||
quantity, unit = uc.merge(1, "pound", 8, "ounce")
|
|
||||||
|
|
||||||
assert isinstance(unit, pint.Unit)
|
|
||||||
# 1 pound + 8 ounces = 16 ounces + 8 ounces = 24 ounces
|
|
||||||
assert str(unit) == "pound"
|
|
||||||
assert quantity == 1.5
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_zero_quantities():
|
|
||||||
uc = UnitConverter()
|
|
||||||
quantity, unit = uc.merge(0, "cup", 1, "cup")
|
|
||||||
|
|
||||||
assert isinstance(unit, pint.Unit)
|
|
||||||
assert str(unit) == "cup"
|
|
||||||
assert quantity == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_invalid_unit():
|
|
||||||
uc = UnitConverter()
|
|
||||||
|
|
||||||
with pytest.raises(UnitNotFound):
|
|
||||||
uc.merge(1, "pound", 1, random_string())
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_incompatible_units():
|
|
||||||
uc = UnitConverter()
|
|
||||||
|
|
||||||
with pytest.raises(pint.errors.DimensionalityError):
|
|
||||||
uc.merge(1, "pound", 1, "cup")
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_negative_quantity():
|
|
||||||
uc = UnitConverter()
|
|
||||||
quantity, unit = uc.merge(-1, "cup", 2, "cup")
|
|
||||||
|
|
||||||
assert isinstance(unit, pint.Unit)
|
|
||||||
assert str(unit) == "cup"
|
|
||||||
assert quantity == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_volume_ounce():
|
|
||||||
uc = UnitConverter()
|
|
||||||
quantity, unit = uc.merge(4, "ounce", 1, "cup")
|
|
||||||
|
|
||||||
assert isinstance(unit, pint.Unit)
|
|
||||||
assert str(unit) == "fluid_ounce" # converted automatically from ounce
|
|
||||||
assert quantity == 12
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_quantity_and_unit_simple():
|
|
||||||
unit_1 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
|
||||||
unit_2 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
|
||||||
|
|
||||||
quantity, unit = merge_quantity_and_unit(1, unit_1, 2, unit_2)
|
|
||||||
|
|
||||||
assert quantity == 3
|
|
||||||
assert unit.name == "mealie_cup"
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_quantity_and_unit_invalid():
|
|
||||||
unit_1 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
|
||||||
unit_2 = CreateIngredientUnit(name="mealie_random", standard_quantity=1, standard_unit=random_string())
|
|
||||||
|
|
||||||
with pytest.raises(UnitNotFound):
|
|
||||||
merge_quantity_and_unit(1, unit_1, 1, unit_2)
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_quantity_and_unit_compatible():
|
|
||||||
unit_1 = CreateIngredientUnit(name="mealie_pint", standard_quantity=1, standard_unit="pint")
|
|
||||||
unit_2 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
|
||||||
|
|
||||||
quantity, unit = merge_quantity_and_unit(1, unit_1, 1, unit_2)
|
|
||||||
|
|
||||||
# 1 pint + 1 cup = 2 pints + 1 cup = 3 cups, converted to pint = 1.5 pint
|
|
||||||
assert quantity == 1.5
|
|
||||||
assert unit.name == "mealie_pint"
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_quantity_and_unit_selects_larger_unit():
|
|
||||||
unit_1 = CreateIngredientUnit(name="mealie_pint", standard_quantity=1, standard_unit="pint")
|
|
||||||
unit_2 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
|
||||||
|
|
||||||
quantity, unit = merge_quantity_and_unit(2, unit_1, 4, unit_2)
|
|
||||||
|
|
||||||
# 2 pint + 4 cup = 4 cups + 4 cups = 8 cups, should be returned as pint (larger unit)
|
|
||||||
assert quantity == 4
|
|
||||||
assert unit.name == "mealie_pint"
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_quantity_and_unit_selects_smaller_unit():
|
|
||||||
unit_1 = CreateIngredientUnit(name="mealie_pint", standard_quantity=1, standard_unit="pint")
|
|
||||||
unit_2 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
|
||||||
|
|
||||||
quantity, unit = merge_quantity_and_unit(0.125, unit_1, 0.5, unit_2)
|
|
||||||
|
|
||||||
# 0.125 pint + 0.5 cup = 0.25 cup + 0.5 cup = 0.75 cup, should be returned as cup (smaller for < 1)
|
|
||||||
assert quantity == 0.75
|
|
||||||
assert unit.name == "mealie_cup"
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_quantity_and_unit_missing_standard_data():
|
|
||||||
unit_1 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
|
||||||
unit_2 = CreateIngredientUnit(name="mealie_cup_no_std", standard_quantity=None, standard_unit=None)
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
merge_quantity_and_unit(1, unit_1, 1, unit_2)
|
|
||||||
|
|
||||||
|
|
||||||
def test_merge_quantity_and_unit_volume_ounce():
|
|
||||||
unit_1 = CreateIngredientUnit(name="mealie_oz", standard_quantity=1, standard_unit="ounce")
|
|
||||||
unit_2 = CreateIngredientUnit(name="mealie_cup", standard_quantity=1, standard_unit="cup")
|
|
||||||
|
|
||||||
quantity, unit = merge_quantity_and_unit(8, unit_1, 1, unit_2)
|
|
||||||
|
|
||||||
assert quantity == 2
|
|
||||||
assert unit.name == "mealie_cup"
|
|
||||||
@@ -1,26 +1,11 @@
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import pytest
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from mealie.repos.all_repositories import AllRepositories, get_repositories
|
|
||||||
from mealie.schema.recipe.recipe import Recipe
|
from mealie.schema.recipe.recipe import Recipe
|
||||||
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient, SaveIngredientUnit
|
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient, SaveIngredientUnit
|
||||||
from mealie.schema.user.user import GroupBase
|
from tests.utils.factories import random_string
|
||||||
from tests.utils.factories import random_int, random_string
|
|
||||||
from tests.utils.fixture_schemas import TestUser
|
from tests.utils.fixture_schemas import TestUser
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def unique_local_group_id(unfiltered_database: AllRepositories) -> str:
|
|
||||||
return str(unfiltered_database.groups.create(GroupBase(name=random_string())).id)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def unique_db(session: Session, unique_local_group_id: str) -> AllRepositories:
|
|
||||||
return get_repositories(session, group_id=unique_local_group_id)
|
|
||||||
|
|
||||||
|
|
||||||
def test_unit_merger(unique_user: TestUser):
|
def test_unit_merger(unique_user: TestUser):
|
||||||
database = unique_user.repos
|
database = unique_user.repos
|
||||||
recipe: Recipe | None = None
|
recipe: Recipe | None = None
|
||||||
@@ -66,79 +51,3 @@ def test_unit_merger(unique_user: TestUser):
|
|||||||
|
|
||||||
for ingredient in recipe.recipe_ingredient:
|
for ingredient in recipe.recipe_ingredient:
|
||||||
assert ingredient.unit.id == unit_1.id # type: ignore
|
assert ingredient.unit.id == unit_1.id # type: ignore
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("standard_field", ["name", "plural_name", "abbreviation", "plural_abbreviation"])
|
|
||||||
@pytest.mark.parametrize("use_bulk", [True, False])
|
|
||||||
def test_auto_inject_standardization(unique_db: AllRepositories, standard_field: str, use_bulk: bool):
|
|
||||||
unit_in = SaveIngredientUnit(name=random_string(), group_id=unique_db.group_id).model_dump()
|
|
||||||
unit_in[standard_field] = "gallon"
|
|
||||||
|
|
||||||
if use_bulk:
|
|
||||||
out_many = unique_db.ingredient_units.create_many([unit_in])
|
|
||||||
assert len(out_many) == 1
|
|
||||||
unit_out = out_many[0]
|
|
||||||
else:
|
|
||||||
unit_out = unique_db.ingredient_units.create(unit_in)
|
|
||||||
|
|
||||||
assert unit_out.standard_unit == "cup"
|
|
||||||
assert unit_out.standard_quantity == 16
|
|
||||||
|
|
||||||
|
|
||||||
def test_dont_auto_inject_random(unique_db: AllRepositories):
|
|
||||||
unit_in = SaveIngredientUnit(name=random_string(), group_id=unique_db.group_id)
|
|
||||||
unit_out = unique_db.ingredient_units.create(unit_in)
|
|
||||||
|
|
||||||
assert unit_out.standard_quantity is None
|
|
||||||
assert unit_out.standard_unit is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_auto_inject_other_language(unique_db: AllRepositories):
|
|
||||||
# Inject custom unit map
|
|
||||||
GALLON = random_string()
|
|
||||||
unique_db.ingredient_units._standardized_unit_map = {GALLON: "gallon"}
|
|
||||||
|
|
||||||
# Create unit with translated value
|
|
||||||
unit_in = SaveIngredientUnit(name=GALLON, group_id=unique_db.group_id)
|
|
||||||
unit_out = unique_db.ingredient_units.create(unit_in)
|
|
||||||
|
|
||||||
assert unit_out.standard_unit == "cup"
|
|
||||||
assert unit_out.standard_quantity == 16
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("name", ["custom-mealie-unit", "gallon"])
|
|
||||||
def test_user_standardization(unique_db: AllRepositories, name: str):
|
|
||||||
unit_in = SaveIngredientUnit(
|
|
||||||
name=name,
|
|
||||||
group_id=unique_db.group_id,
|
|
||||||
standard_quantity=random_int(1, 10),
|
|
||||||
standard_unit=random_string(),
|
|
||||||
)
|
|
||||||
unit_out = unique_db.ingredient_units.create(unit_in)
|
|
||||||
|
|
||||||
assert unit_out.standard_quantity == unit_in.standard_quantity
|
|
||||||
assert unit_out.standard_unit == unit_in.standard_unit
|
|
||||||
|
|
||||||
|
|
||||||
def test_ignore_incomplete_standardization(unique_db: AllRepositories):
|
|
||||||
unit_in = SaveIngredientUnit(
|
|
||||||
name=random_string(),
|
|
||||||
group_id=unique_db.group_id,
|
|
||||||
standard_quantity=random_int(1, 10),
|
|
||||||
standard_unit=None,
|
|
||||||
)
|
|
||||||
unit_out = unique_db.ingredient_units.create(unit_in)
|
|
||||||
|
|
||||||
assert unit_out.standard_quantity is None
|
|
||||||
assert unit_out.standard_unit is None
|
|
||||||
|
|
||||||
unit_in = SaveIngredientUnit(
|
|
||||||
name=random_string(),
|
|
||||||
group_id=unique_db.group_id,
|
|
||||||
standard_quantity=None,
|
|
||||||
standard_unit=random_string(),
|
|
||||||
)
|
|
||||||
unit_out = unique_db.ingredient_units.create(unit_in)
|
|
||||||
|
|
||||||
assert unit_out.standard_quantity is None
|
|
||||||
assert unit_out.standard_unit is None
|
|
||||||
|
|||||||
@@ -217,22 +217,6 @@ def _b9e516e2d3b3_add_household_to_recipe_last_made_household_to_foods_and_tools
|
|||||||
assert not tool.households_with_tool
|
assert not tool.households_with_tool
|
||||||
|
|
||||||
|
|
||||||
def _a39c7f1826e3_add_unit_standardization_fields(session: Session):
|
|
||||||
groups = session.query(Group).all()
|
|
||||||
|
|
||||||
for group in groups:
|
|
||||||
# test_data.backup_version_1d9a002d7234_1 has a non-anonymized "pint" unit
|
|
||||||
# and has not yet run the standardization migration.
|
|
||||||
pint_units = (
|
|
||||||
session.query(IngredientUnitModel)
|
|
||||||
.filter(IngredientUnitModel.group_id == group.id, IngredientUnitModel.name == "pint")
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
for unit in pint_units:
|
|
||||||
assert unit.standard_quantity == 2
|
|
||||||
assert unit.standard_unit == "cup"
|
|
||||||
|
|
||||||
|
|
||||||
def test_database_restore_data():
|
def test_database_restore_data():
|
||||||
"""
|
"""
|
||||||
This tests real user backups to make sure the data is restored correctly. The data has been anonymized, but
|
This tests real user backups to make sure the data is restored correctly. The data has been anonymized, but
|
||||||
@@ -243,7 +227,6 @@ def test_database_restore_data():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
backup_paths = [
|
backup_paths = [
|
||||||
test_data.backup_version_1d9a002d7234_1,
|
|
||||||
test_data.backup_version_44e8d670719d_1,
|
test_data.backup_version_44e8d670719d_1,
|
||||||
test_data.backup_version_44e8d670719d_2,
|
test_data.backup_version_44e8d670719d_2,
|
||||||
test_data.backup_version_44e8d670719d_3,
|
test_data.backup_version_44e8d670719d_3,
|
||||||
@@ -262,7 +245,6 @@ def test_database_restore_data():
|
|||||||
_d7c6efd2de42_migrate_favorites_and_ratings_to_user_ratings,
|
_d7c6efd2de42_migrate_favorites_and_ratings_to_user_ratings,
|
||||||
_86054b40fd06_added_query_filter_string_to_cookbook_and_mealplan,
|
_86054b40fd06_added_query_filter_string_to_cookbook_and_mealplan,
|
||||||
_b9e516e2d3b3_add_household_to_recipe_last_made_household_to_foods_and_tools,
|
_b9e516e2d3b3_add_household_to_recipe_last_made_household_to_foods_and_tools,
|
||||||
_a39c7f1826e3_add_unit_standardization_fields,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
settings = get_app_settings()
|
settings = get_app_settings()
|
||||||
|
|||||||
42
uv.lock
generated
42
uv.lock
generated
@@ -850,7 +850,6 @@ dependencies = [
|
|||||||
{ name = "paho-mqtt" },
|
{ name = "paho-mqtt" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "pillow-heif" },
|
{ name = "pillow-heif" },
|
||||||
{ name = "pint" },
|
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "pyhumps" },
|
{ name = "pyhumps" },
|
||||||
@@ -924,7 +923,6 @@ requires-dist = [
|
|||||||
{ name = "paho-mqtt", specifier = "==1.6.1" },
|
{ name = "paho-mqtt", specifier = "==1.6.1" },
|
||||||
{ name = "pillow", specifier = "==12.1.1" },
|
{ name = "pillow", specifier = "==12.1.1" },
|
||||||
{ name = "pillow-heif", specifier = "==1.2.1" },
|
{ name = "pillow-heif", specifier = "==1.2.1" },
|
||||||
{ name = "pint", specifier = ">=0.25" },
|
|
||||||
{ name = "psycopg2-binary", marker = "extra == 'pgsql'", specifier = "==2.9.11" },
|
{ name = "psycopg2-binary", marker = "extra == 'pgsql'", specifier = "==2.9.11" },
|
||||||
{ name = "pydantic", specifier = "==2.12.5" },
|
{ name = "pydantic", specifier = "==2.12.5" },
|
||||||
{ name = "pydantic-settings", specifier = "==2.13.1" },
|
{ name = "pydantic-settings", specifier = "==2.13.1" },
|
||||||
@@ -960,7 +958,7 @@ dev = [
|
|||||||
{ name = "pytest", specifier = "==9.0.2" },
|
{ name = "pytest", specifier = "==9.0.2" },
|
||||||
{ name = "pytest-asyncio", specifier = "==1.3.0" },
|
{ name = "pytest-asyncio", specifier = "==1.3.0" },
|
||||||
{ name = "rich", specifier = "==14.3.3" },
|
{ name = "rich", specifier = "==14.3.3" },
|
||||||
{ name = "ruff", specifier = "==0.15.2" },
|
{ name = "ruff", specifier = "==0.15.3" },
|
||||||
{ name = "types-python-dateutil", specifier = "==2.9.0.20260124" },
|
{ name = "types-python-dateutil", specifier = "==2.9.0.20260124" },
|
||||||
{ name = "types-python-slugify", specifier = "==8.0.2.20240310" },
|
{ name = "types-python-slugify", specifier = "==8.0.2.20240310" },
|
||||||
{ name = "types-pyyaml", specifier = "==6.0.12.20250915" },
|
{ name = "types-pyyaml", specifier = "==6.0.12.20250915" },
|
||||||
@@ -1742,27 +1740,27 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.15.2"
|
version = "0.15.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/3c/3b/20d9a0bc954d51b63f20cf710cf506bfe675d1e6138139342dd5ccc90326/ruff-0.15.3.tar.gz", hash = "sha256:78757853320d8ddb9da24e614ef69a37bcbcfd477e5a6435681188d4bce4eaa1", size = 4569031, upload-time = "2026-02-26T15:39:38.015Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" },
|
{ url = "https://files.pythonhosted.org/packages/ed/00/c544ab1d70f86dc50a2f2a8e1262e5af5025897ccd820415f559f9f2f63f/ruff-0.15.3-py3-none-linux_armv6l.whl", hash = "sha256:f7df0fd6f889a8d8de2ddb48a9eb55150954400f2157ea15b21a2f49ecaaf988", size = 10444066, upload-time = "2026-02-26T15:39:47.708Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" },
|
{ url = "https://files.pythonhosted.org/packages/fb/15/9dee3f4e891261adbd690f8c6f075418a7cd76e845601b00a0da2ae2ad6e/ruff-0.15.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0198b5445197d443c3bbf2cc358f4bd477fb3951e3c7f2babc13e9bb490614a8", size = 10853125, upload-time = "2026-02-26T15:40:18.943Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" },
|
{ url = "https://files.pythonhosted.org/packages/88/ba/fc5aeda852c89faf821d36c951df866117342e88439e1b1e1e762a07b7fd/ruff-0.15.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:adf95b5be57b25fbbbc07cd68d37414bee8729e807ad0217219558027186967e", size = 10180833, upload-time = "2026-02-26T15:40:13.282Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" },
|
{ url = "https://files.pythonhosted.org/packages/06/87/e2f80a39164476fac4d45752a0d4721d6645f40b7f851e48add12af9947e/ruff-0.15.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b56dbd9cd86489ccbad96bb58fa4c958342b5510fdeb60ea13d9d3566bd845c", size = 10536806, upload-time = "2026-02-26T15:40:24.129Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" },
|
{ url = "https://files.pythonhosted.org/packages/fd/89/2e5bf0ed30ea3778460ea4d8cc6cb4d88ba96d9732d2c0cc33349cd65196/ruff-0.15.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6f263ce511871955d8c5401b62c7e863988ea4d0527aa0a3b1b7ecff4d4abc4", size = 10276093, upload-time = "2026-02-26T15:39:44.654Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" },
|
{ url = "https://files.pythonhosted.org/packages/82/cb/318206d778c7f42917ca7b0f9436cf27652d1731fe434d3c9990c4a611fa/ruff-0.15.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e90fa1bed82ffede5768232b9bd23212c547ab7cd74c752007ecade1d895ee1a", size = 11051593, upload-time = "2026-02-26T15:39:35.157Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" },
|
{ url = "https://files.pythonhosted.org/packages/58/8f/65ee4c1b88e49dd4c0a3fc43e81832536c7942f0c702b6f3d25db0f95d6c/ruff-0.15.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e9d53760b7061ddbe5ea9e25381332c607fc14c40bde78f8a25392a93a68d74", size = 11885820, upload-time = "2026-02-26T15:39:59.504Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" },
|
{ url = "https://files.pythonhosted.org/packages/db/04/d4261f6729ad9a356bc6e3223ba297acf3b66118cef4795b4a8953b255ff/ruff-0.15.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec90e3b78c56c4acca4264d371dd48e29215ecb673cc2fa3c4b799b72050e491", size = 11340583, upload-time = "2026-02-26T15:39:50.781Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/84/490f38b2bc104e0fdc9496c2a66a48fb2d24a01de46ba0c60c4f6c4d4590/ruff-0.15.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7ce448fd395f822e34c8f6f7dfcd84b6726340082950858f92c4daa6baf8915", size = 11160701, upload-time = "2026-02-26T15:40:02.447Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" },
|
{ url = "https://files.pythonhosted.org/packages/ad/25/eae9cb7b6c28b425ed8cbe797da89c78146071102181ba74c4cdfd06bbeb/ruff-0.15.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14f7d763962d385f75b9b3b57fcc5661c56c20d8b1ddc9f5c881b5fa0ba499fa", size = 11111482, upload-time = "2026-02-26T15:39:56.462Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" },
|
{ url = "https://files.pythonhosted.org/packages/95/18/16d0b5ef143cb9e52724f18cbccb4b3c5cd4d4e2debbd95e2be3aeb64c9e/ruff-0.15.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b57084e3a3d65418d376c7023711c37cce023cd2fb038a76ba15ee21f3c2c2ee", size = 10497151, upload-time = "2026-02-26T15:40:10.64Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" },
|
{ url = "https://files.pythonhosted.org/packages/bf/b4/1829314241ddba07c54a742ab387da343fe56a0267a6b6498f3e2ae99821/ruff-0.15.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d567523ff7dcf3112b0f71231d18c3506dd06943359476ee64dea0f9c8f63976", size = 10281955, upload-time = "2026-02-26T15:40:16.033Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" },
|
{ url = "https://files.pythonhosted.org/packages/d7/93/80a4ec4bd3cf58ca9b49dccf2bd232b520db14184912fb7e0eb6f3ecc484/ruff-0.15.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4223088d255bf31a50b6640445b39f668164d64c23e5fa403edfb1e0b11122e5", size = 10766613, upload-time = "2026-02-26T15:40:21.55Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" },
|
{ url = "https://files.pythonhosted.org/packages/da/92/fe016b862295dc57499997e7f2edc58119469b210f4f03ccb763fa65f130/ruff-0.15.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:32399ddae088970b2db6efd8d3f49981375cb828075359b6c088ed1fe63d64e1", size = 11262113, upload-time = "2026-02-26T15:39:41.5Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" },
|
{ url = "https://files.pythonhosted.org/packages/42/b1/77dcd05940388d9ba3de03ac4b8b598826d57935728071e1be9f2ef5b714/ruff-0.15.3-py3-none-win32.whl", hash = "sha256:1f1eb95ff614351e3a89a862b6d94e6c42c170e61916e1f20facd6c38477f5f3", size = 10509423, upload-time = "2026-02-26T15:40:05.217Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" },
|
{ url = "https://files.pythonhosted.org/packages/29/d5/76aab0fabbd54e8c77d02fcff2494906ba85b539d22aa9b7124f7100f008/ruff-0.15.3-py3-none-win_amd64.whl", hash = "sha256:2b22dffe5f5e1e537097aa5208684f069e495f980379c4491b1cfb198a444d0c", size = 11637739, upload-time = "2026-02-26T15:39:53.951Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
|
{ url = "https://files.pythonhosted.org/packages/f2/61/9b4e3682dfd26054321e1b2fdb67a51361dd6ec2fb63f2b50d711f8832ae/ruff-0.15.3-py3-none-win_arm64.whl", hash = "sha256:82443c14d694d4cbd9e598ede27ef5d6f08389ccad91c933be775ea2f4e66f76", size = 10957794, upload-time = "2026-02-26T15:40:08.045Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user