mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-06-13 12:30:14 -04:00
Compare commits
6 Commits
v3.18.0
...
fix/6853-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f644ee1879 | ||
|
|
8eb00c3dc0 | ||
|
|
642c826f2b | ||
|
|
493154caa8 | ||
|
|
71e0d99a46 | ||
|
|
c52a4e10c9 |
@@ -205,7 +205,7 @@ const createLinks = computed(() => [
|
||||
insertDivider: false,
|
||||
icon: $globals.icons.fileImage,
|
||||
title: i18n.t("recipe.create-from-images"),
|
||||
subtitle: i18n.t("recipe.create-recipe-from-an-image"),
|
||||
subtitle: i18n.t("recipe.create-recipe-from-images"),
|
||||
to: `/g/${groupSlug.value}/r/create/image`,
|
||||
restricted: true,
|
||||
hide: !showImageImport.value,
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Create a new recipe from scratch.",
|
||||
"create-recipes": "Create Recipes",
|
||||
"import-with-zip": "Voer in met .zip",
|
||||
"create-recipe-from-an-image": "Create Recipe from an Image",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.",
|
||||
"crop-and-rotate-the-image": "Crop and rotate the image so that only the text is visible, and it's in the correct orientation.",
|
||||
"create-from-images": "Create from Images",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "إنشاء وصفة جديدة من الصفر.",
|
||||
"create-recipes": "إنشاء الوصفات",
|
||||
"import-with-zip": "الاستيراد باستخدام zip.",
|
||||
"create-recipe-from-an-image": "إنشاء وصفة عن طريق صورة",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.",
|
||||
"crop-and-rotate-the-image": "قم بقص الصورة وتدويرها بحيث يظهر النص فقط، ويكون في الاتجاه الصحيح.",
|
||||
"create-from-images": "إنشاء عن طريق صور",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Създайте нова рецепта от чернова.",
|
||||
"create-recipes": "Създайте рецепти",
|
||||
"import-with-zip": "Импортирай от .zip",
|
||||
"create-recipe-from-an-image": "Create Recipe from an Image",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.",
|
||||
"crop-and-rotate-the-image": "Изрежете и завъртете изображението, така че да се вижда само текстът и той да е в правилната ориентация.",
|
||||
"create-from-images": "Създаване от изображения",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Crea una nova recepta des de zero.",
|
||||
"create-recipes": "Crea Receptes",
|
||||
"import-with-zip": "Importar amb un .zip",
|
||||
"create-recipe-from-an-image": "Crear una recepta a partir d'una imatge",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Crear una recepta pujant una imatge d'ella. Mealie intentarà extreure el text de la imatge mitjançant IA i crear-ne la recepta.",
|
||||
"crop-and-rotate-the-image": "Retalla i rota la imatge, per tal que només el text sigui visible, i estigui orientat correctament.",
|
||||
"create-from-images": "Crear una recepta a partir d'una imatge",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Vytvořit nový recept od nuly.",
|
||||
"create-recipes": "Vytvořit recepty",
|
||||
"import-with-zip": "Importovat pomocí .zip",
|
||||
"create-recipe-from-an-image": "Vytvořit recept z obrázku",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Vytvořte recept nahráním obrázku. Mealie se pokusí z obrázku extrahovat text pomocí AI a vytvořit z něj recept.",
|
||||
"crop-and-rotate-the-image": "Oříznout a otočit obrázek tak, aby byl viditelný pouze text a aby byl ve správné orientaci.",
|
||||
"create-from-images": "Vytvořit z obrázků",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Opret ny opskrift fra bunden.",
|
||||
"create-recipes": "Opret opskrift",
|
||||
"import-with-zip": "Importér fra ZIP-fil",
|
||||
"create-recipe-from-an-image": "Opret opskrift fra et billede",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Opret en opskrift ved at overføre et billede af den. Mealie vil forsøge at udtrække teksten fra billedet med AI og oprette en opskrift fra det.",
|
||||
"crop-and-rotate-the-image": "Beskær og roter billedet, så kun teksten er synlig, og det vises i den rigtige retning.",
|
||||
"create-from-images": "Opret fra billede",
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
"token": "Token",
|
||||
"tuesday": "Dienstag",
|
||||
"type": "Typ",
|
||||
"undo": "Undo",
|
||||
"undo": "Rückgängig",
|
||||
"update": "Aktualisieren",
|
||||
"updated": "Aktualisiert",
|
||||
"upload": "Hochladen",
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Erstelle ein neues Rezept von Grund auf.",
|
||||
"create-recipes": "Rezepte erstellen",
|
||||
"import-with-zip": "Von .zip importieren",
|
||||
"create-recipe-from-an-image": "Rezept von einem Bild erstellen",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Erstelle ein Rezept, indem du ein Bild hochlädst. Mealie wird versuchen, den Text aus dem Bild mit Hilfe von KI zu extrahieren und ein Rezept daraus zu erstellen.",
|
||||
"crop-and-rotate-the-image": "Beschneide und drehe das Bild so, dass nur der Text zu sehen ist und die Ausrichtung stimmt.",
|
||||
"create-from-images": "Aus Bildern erstellen",
|
||||
@@ -917,7 +917,7 @@
|
||||
"quantity": "Menge: {0}",
|
||||
"shopping-list": "Einkaufsliste",
|
||||
"shopping-lists": "Einkaufslisten",
|
||||
"add-item": "Add item",
|
||||
"add-item": "Eintrag hinzufügen",
|
||||
"food": "Lebensmittel",
|
||||
"note": "Notiz",
|
||||
"label": "Kategorie",
|
||||
@@ -1478,10 +1478,10 @@
|
||||
"max-length": "Muss maximal {max} Zeichen haben|Muss maximal {max} Zeichen haben"
|
||||
},
|
||||
"announcements": {
|
||||
"announcements": "Announcements",
|
||||
"all-announcements": "All announcements",
|
||||
"announcements": "Ankündigungen",
|
||||
"all-announcements": "Alle Ankündigungen",
|
||||
"mark-all-as-read": "Alle als gelesen markieren",
|
||||
"show-announcements-from-mealie": "Show announcements from Mealie",
|
||||
"show-announcements-setting-description": "Whether or not you want to allow users to see announcements from Mealie. When enabled users can still opt-out from seeing them in their user settings"
|
||||
"show-announcements-from-mealie": "Ankündigung von Mealie anzeigen",
|
||||
"show-announcements-setting-description": "Lege fest, ob Benutzer Ankündigungen von Mealie sehen dürfen. Wenn aktiviert, können Benutzer die Anzeige in ihren Benutzereinstellungen immer noch deaktivieren"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Δημιουργήστε μια νέα συνταγή από το μηδέν.",
|
||||
"create-recipes": "Δημιουργία Συνταγών",
|
||||
"import-with-zip": "Εισαγωγή μέσω .zip",
|
||||
"create-recipe-from-an-image": "Δημιουργία συνταγής από μια εικόνα",
|
||||
"create-recipe-from-images": "Δημιουργία συνταγής από εικόνες",
|
||||
"create-recipe-from-an-image-description": "Δημιουργήστε μια συνταγή ανεβάζοντας μια εικόνα της. Το Mealie θα προσπαθήσει να εξάγει το κείμενο από την εικόνα χρησιμοποιώντας τεχνητή νοημοσύνη και να δημιουργήσει μια συνταγή από αυτό.",
|
||||
"crop-and-rotate-the-image": "Περικοπή και περιστροφή της εικόνας, έτσι ώστε να είναι μόνο το κείμενο ορατό και να είναι στο σωστό προσανατολισμό.",
|
||||
"create-from-images": "Δημιουργία από εικόνες",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Create a new recipe from scratch.",
|
||||
"create-recipes": "Create Recipes",
|
||||
"import-with-zip": "Import with .zip",
|
||||
"create-recipe-from-an-image": "Create Recipe from an Image",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.",
|
||||
"crop-and-rotate-the-image": "Crop and rotate the image so that only the text is visible, and it's in the correct orientation.",
|
||||
"create-from-images": "Create from Images",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Create a new recipe from scratch.",
|
||||
"create-recipes": "Create Recipes",
|
||||
"import-with-zip": "Import with .zip",
|
||||
"create-recipe-from-an-image": "Create Recipe from Images",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Create a recipe by uploading images of the recipe text. Mealie will attempt to extract the text from the images using AI and create a new recipe from it.",
|
||||
"crop-and-rotate-the-image": "Crop and rotate the image so that only the text is visible, and it's in the correct orientation.",
|
||||
"create-from-images": "Create from Images",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Crear nueva receta desde cero.",
|
||||
"create-recipes": "Crear Recetas",
|
||||
"import-with-zip": "Importar desde .zip",
|
||||
"create-recipe-from-an-image": "Crear receta a partir de una imagen",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Crea una receta cargando una imagen de ella. Mealie intentará extraer el texto de la imagen usando IA y crear una receta de ella.",
|
||||
"crop-and-rotate-the-image": "Recortar y rotar la imagen de manera que sólo el texto sea visible, y esté en la orientación correcta.",
|
||||
"create-from-images": "Crear a partir de imágenes",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Loo uus retsept algusest",
|
||||
"create-recipes": "Loo retseptid",
|
||||
"import-with-zip": "Impordi .zip failist",
|
||||
"create-recipe-from-an-image": "Retsepti loomine pildist",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Retsepti loomiseks lae üles selle pilt. Mealie üritab ekstraheerida pildil oleva teksti ning luua retsepti sellest kasutades AI-d.",
|
||||
"crop-and-rotate-the-image": "Kärpige ja pöörake pilti nii, et ainult tekst oleks nähtaval ja see oleks suunatud ülespoole.",
|
||||
"create-from-images": "Retsepti loomine pildist",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Luo resepti alusta.",
|
||||
"create-recipes": "Luo reseptejä",
|
||||
"import-with-zip": "Tuo .zip:llä",
|
||||
"create-recipe-from-an-image": "Luo resepti kuvasta",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Luo resepti tuomalla siitä kuva. Mealie pyrkii poimimaan tekstin kuvasta tekoälyllä ja luomaan siitä reseptin.",
|
||||
"crop-and-rotate-the-image": "Rajaa ja kierrä kuvaa niin, että vain teksti näkyy, ja että se on oikein päin.",
|
||||
"create-from-images": "Luo resepti kuvasta",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Créer une nouvelle recette de zéro.",
|
||||
"create-recipes": "Créer des recettes",
|
||||
"import-with-zip": "Importer un .zip",
|
||||
"create-recipe-from-an-image": "Créer une recette à partir d’une image",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Créez une recette en téléchargeant une image de celle-ci. Mealie utilisera l’IA pour tenter d’extraire le texte et de créer une recette.",
|
||||
"crop-and-rotate-the-image": "Rogner et pivoter l’image pour que seul le texte soit visible, et qu’il soit dans la bonne orientation.",
|
||||
"create-from-images": "Créer à partir d’une image",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Créer une nouvelle recette à partir de zéro.",
|
||||
"create-recipes": "Créer des recettes",
|
||||
"import-with-zip": "Importer un .zip",
|
||||
"create-recipe-from-an-image": "Créer une recette à partir d’une image",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Créez une recette en téléversant une image de celle-ci. Mealie utilisera l’IA pour tenter d’extraire le texte et de créer une recette.",
|
||||
"crop-and-rotate-the-image": "Rogner et pivoter l’image pour que seul le texte soit visible et qu’il soit dans la bonne orientation.",
|
||||
"create-from-images": "Créer à partir d’images",
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
"token": "Jeton",
|
||||
"tuesday": "Mardi",
|
||||
"type": "Type",
|
||||
"undo": "Undo",
|
||||
"undo": "Annuler",
|
||||
"update": "Mettre à jour",
|
||||
"updated": "Mis à jour",
|
||||
"upload": "Importer",
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Créer une nouvelle recette de zéro.",
|
||||
"create-recipes": "Créer des recettes",
|
||||
"import-with-zip": "Importer un .zip",
|
||||
"create-recipe-from-an-image": "Créer une recette à partir d’une image",
|
||||
"create-recipe-from-images": "Créer une recette depuis une image",
|
||||
"create-recipe-from-an-image-description": "Créez une recette en téléchargeant une image de celle-ci. Mealie utilisera l’IA pour tenter d’extraire le texte et de créer une recette.",
|
||||
"crop-and-rotate-the-image": "Rogner et pivoter l’image pour que seul le texte soit visible, et qu’il soit dans la bonne orientation.",
|
||||
"create-from-images": "Créer à partir d’images",
|
||||
@@ -943,7 +943,7 @@
|
||||
"are-you-sure-you-want-to-uncheck-all-items": "Voulez-vous vraiment désélectionner tous les éléments ?",
|
||||
"are-you-sure-you-want-to-delete-checked-items": "Voulez-vous vraiment supprimer tous les éléments sélectionnés ?",
|
||||
"no-shopping-lists-found": "Aucune liste de courses trouvée",
|
||||
"item-checked-off": "Checked off {item}"
|
||||
"item-checked-off": "{item} coché"
|
||||
},
|
||||
"sidebar": {
|
||||
"all-recipes": "Recettes",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Crear unha receita en branco.",
|
||||
"create-recipes": "Crear Receitas",
|
||||
"import-with-zip": "Importar con .zip",
|
||||
"create-recipe-from-an-image": "Crear receita a partir dunha imaxen",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Cree unha receita cargando unha imaxen da mesma. O Mealie tentará extrair o texto da imaxen utilizando IA e creará unha receita a partir da mesma.",
|
||||
"crop-and-rotate-the-image": "Recorte e vire a imaxen de modo a que só o texto sexa visível e na orientación correta.",
|
||||
"create-from-images": "Crear a partir de imaxens",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "יצירת מתכון חדש מאפס.",
|
||||
"create-recipes": "יצירת מתכונים",
|
||||
"import-with-zip": "ייבא באמצעות zip",
|
||||
"create-recipe-from-an-image": "יצירת מתכון מתמונה",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "יצירת מתכון ע\"י העלאת תמונה שלו. Mealie תנסה לחלץ את הטקסט מהתמונה באמצעות AI ותייצר ממנו מתכון.",
|
||||
"crop-and-rotate-the-image": "נא לחתוך ולסובב את התמונה כך שרואים רק את הטקסט, והוא בכיוון הנכון.",
|
||||
"create-from-images": "יצירה מתמונה",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Izradi novi recept od početka",
|
||||
"create-recipes": "Kreiraj recept",
|
||||
"import-with-zip": "Učitaj pomoću .zip-a",
|
||||
"create-recipe-from-an-image": "Create Recipe from an Image",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.",
|
||||
"crop-and-rotate-the-image": "Obreži i rotiraj sliku tako da bude vidljiv samo tekst i da bude u ispravnoj orijentaciji.",
|
||||
"create-from-images": "Izradi na temelju fotografije",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Adj hozzá egy új receptet a nulláról kezdve.",
|
||||
"create-recipes": "Receptek létrehozása",
|
||||
"import-with-zip": "Importálás .zip formátummal",
|
||||
"create-recipe-from-an-image": "Recept készítése képről",
|
||||
"create-recipe-from-images": "Recept létrehozása képek alapján",
|
||||
"create-recipe-from-an-image-description": "Hozzon létre egy receptet egy kép feltöltésével. A Mealie megpróbálja a kép szövegét mesterséges intelligencia segítségével kinyerni, és létrehozni belőle a receptet.",
|
||||
"crop-and-rotate-the-image": "Vágja ki és forgassa el a képet úgy, hogy csak a szöveg legyen látható, és megfelelő tájolásban legyen.",
|
||||
"create-from-images": "Létrehozás képekről",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Stofna nýja uppskrift frá grunni.",
|
||||
"create-recipes": "Stofna uppskriftir",
|
||||
"import-with-zip": "Hlaða inn með .zip",
|
||||
"create-recipe-from-an-image": "Stofna uppskrift út frá mynd",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Stofna uppskrift með því hlaða inn myndum af uppskriftartextanum. Mealie mun reyna að vinna texta úr myndunum með gervigreind og stofna nýja uppskrift út frá textanum.",
|
||||
"crop-and-rotate-the-image": "Sníða og snúa mynd svo bara textinn sé sýnilegur og að myndin snúi rétt.",
|
||||
"create-from-images": "Stofna uppskrift frá mynd",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Crea una nuova ricetta da zero.",
|
||||
"create-recipes": "Crea Ricette",
|
||||
"import-with-zip": "Importa da .zip",
|
||||
"create-recipe-from-an-image": "Crea ricetta da un'immagine",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Crea una ricetta caricando un'immagine di essa. Mealie tenterà di estrarre il testo dall'immagine usando l'IA e creare una ricetta da esso.",
|
||||
"crop-and-rotate-the-image": "Ritaglia e ruota l'immagine in modo che solo il testo sia visibile e che sia orientato correttamente.",
|
||||
"create-from-images": "Crea da immagini",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "新しいレシピを一から作成します。",
|
||||
"create-recipes": "レシピを作成する",
|
||||
"import-with-zip": ".zip でインポート",
|
||||
"create-recipe-from-an-image": "画像からレシピを作成",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "画像をアップロードしてレシピを作成します。 Mealieは、AIを使用して画像からテキストを抽出し、そこからレシピを作成しようとします。",
|
||||
"crop-and-rotate-the-image": "テキストのみが表示され、正しい方向になるように画像をトリミングして回転します。",
|
||||
"create-from-images": "Create from Images",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "처음부터 새로운 레시피를 만드세요.",
|
||||
"create-recipes": "레시피 생성",
|
||||
"import-with-zip": ".zip 파일로 가져오기",
|
||||
"create-recipe-from-an-image": "이미지에서 레시피 생성",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "레시피 텍스트 이미지를 업로드하여 레시피를 생성하세요. Mealie는 AI를 사용하여 이미지에서 텍스트를 추출하고 이를 통해 새로운 레시피를 생성하려고 시도합니다.",
|
||||
"crop-and-rotate-the-image": "이미지를 잘라내고 회전시켜 텍스트만 보이도록 하고 올바른 방향으로 배치하십시오.",
|
||||
"create-from-images": "이미지에서 생성",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Create a new recipe from scratch.",
|
||||
"create-recipes": "Create Recipes",
|
||||
"import-with-zip": "Įkelti naudojant .zip failus",
|
||||
"create-recipe-from-an-image": "Create Recipe from an Image",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.",
|
||||
"crop-and-rotate-the-image": "Crop and rotate the image so that only the text is visible, and it's in the correct orientation.",
|
||||
"create-from-images": "Kurti iš vaizdų",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Izveidojiet jaunu recepti no nulles.",
|
||||
"create-recipes": "Izveidojiet receptes",
|
||||
"import-with-zip": "Importēt ar .zip",
|
||||
"create-recipe-from-an-image": "Izveidojiet recepti no attēla",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Izveidojiet recepti, augšupielādējot tās attēlu. Mealie mēģinās iegūt tekstu no attēla, izmantojot AI, un no tā izveidot recepti.",
|
||||
"crop-and-rotate-the-image": "Apgrieziet un pagrieziet attēlu tā, lai būtu redzams tikai teksts un tas būtu pareizajā orientācijā.",
|
||||
"create-from-images": "Create from Images",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Maak een nieuw recept.",
|
||||
"create-recipes": "Recepten aanmaken",
|
||||
"import-with-zip": "Importeer met .zip",
|
||||
"create-recipe-from-an-image": "Maak recept van de tekst op een afbeelding",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Maak een recept door een afbeelding ervan te uploaden. Mealie probeert de tekst met behulp van AI uit de afbeelding te halen en er een recept uit te maken.",
|
||||
"crop-and-rotate-the-image": "Snijd de afbeelding bij zodat alleen tekst zichtbaar is. En draai t plaatje zodat het leesbaar is.",
|
||||
"create-from-images": "Maak recept van een afbeelding",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Opprett en ny oppskrift fra bunnen av.",
|
||||
"create-recipes": "Opprett oppskrifter",
|
||||
"import-with-zip": "Importer fra .zip-fil",
|
||||
"create-recipe-from-an-image": "Opprett oppskrift fra et bilde",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Opprett en oppskrift ved å laste opp et bilde av den. Mealie vil forsøke å hente ut teksten fra bildet ved bruk av AI, og lage en ny oppskrift.",
|
||||
"crop-and-rotate-the-image": "Beskjær og roter bildet slik at bare teksten er synlig, og at det er i riktig retning.",
|
||||
"create-from-images": "Opprett fra bilde",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Utwórz nowy przepis od zera.",
|
||||
"create-recipes": "Utwórz przepisy",
|
||||
"import-with-zip": "Importuj z pliku .zip",
|
||||
"create-recipe-from-an-image": "Utwórz przepis z obrazów",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Utwórz przepis poprzez przesłanie obrazów tekstu przepisu. Mealie spróbuje wyodrębnić tekst z obrazów za pomocą AI i utworzyć z niego przepis.",
|
||||
"crop-and-rotate-the-image": "Przytnij i obróć obraz, tak aby był w odpowiedniej orientacji i był widoczny tylko tekst.",
|
||||
"create-from-images": "Utwórz przepis z obrazów",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Criar uma receita do zero.",
|
||||
"create-recipes": "Criar Receitas",
|
||||
"import-with-zip": "Importar a partir de .zip",
|
||||
"create-recipe-from-an-image": "Create Recipe from an Image",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.",
|
||||
"crop-and-rotate-the-image": "Corte e gire a imagem para que apenas o texto esteja visível e esteja na posição correta.",
|
||||
"create-from-images": "Criar a partir de imagens",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Criar uma receita em branco.",
|
||||
"create-recipes": "Criar Receitas",
|
||||
"import-with-zip": "Importar com .zip",
|
||||
"create-recipe-from-an-image": "Criar receita a partir de uma imagem",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Crie uma receita carregando uma imagem da mesma. O Mealie tentará extrair o texto da imagem utilizando IA e criará uma receita a partir da mesma.",
|
||||
"crop-and-rotate-the-image": "Recorte e rode a imagem de modo a que apenas o texto seja visível e esteja na orientação correta.",
|
||||
"create-from-images": "Criar a partir de Imagens",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Creează o rețetă nouă de la zero.",
|
||||
"create-recipes": "Crează rețetă",
|
||||
"import-with-zip": "Importă cu .zip",
|
||||
"create-recipe-from-an-image": "Creează o rețetă dintr-o imagine",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Creează o rețetă prin încărcarea unei imagini a acesteia. Mealie va încerca să extragă textul din imagine folosind AI și să creeze o rețetă din el.",
|
||||
"crop-and-rotate-the-image": "Decupați și rotiți imaginea astfel încât numai textul să fie vizibil, iar orientarea să fie corectă.",
|
||||
"create-from-images": "Creează din Imagini",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Создать новый рецепт с нуля.",
|
||||
"create-recipes": "Создать Рецепт",
|
||||
"import-with-zip": "Импорт из .zip",
|
||||
"create-recipe-from-an-image": "Создать рецепт из изображения",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Создайте рецепт, загрузив изображение. Mealie попытается извлечь текст из изображения с помощью ИИ и создать рецепт из него.",
|
||||
"crop-and-rotate-the-image": "Обрежьте и поверните изображение так, чтобы виден был только текст в нужной ориентации.",
|
||||
"create-from-images": "Создать из изображений",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Vytvoriť nový recept od začiatku.",
|
||||
"create-recipes": "Vytvoriť recept",
|
||||
"import-with-zip": "Importovať .zip súbor",
|
||||
"create-recipe-from-an-image": "Vytvoriť recept z obrázka",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Vytvoriť recept nahraním fotografie jedla. Mealie sa pokúsi previesť obrázok na text pomocou AI a vytvorí k nemu recept.",
|
||||
"crop-and-rotate-the-image": "Orežte a otočte obrázok tak, aby bol viditeľný iba text a aby mal správnu orientáciu.",
|
||||
"create-from-images": "Vytvoriť z obrázka",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Ustvari nov recept.",
|
||||
"create-recipes": "Ustvari recepte",
|
||||
"import-with-zip": "Uvozi z .zip",
|
||||
"create-recipe-from-an-image": "Ustvari recept iz slike",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Ustvarite recept tako, da naložite njegovo sliko. Mealie bo s pomočjo umetne inteligence poskušal izluščiti besedilo iz slike in iz njega ustvariti recept.",
|
||||
"crop-and-rotate-the-image": "Obrežite in zasukajte sliko, tako da bo vidno samo besedilo in da bo v pravilnem položaju.",
|
||||
"create-from-images": "Ustvari iz slik",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Креирајте нови рецепт од нуле.",
|
||||
"create-recipes": "Направите рецепте",
|
||||
"import-with-zip": "Увези помоћу .zip архиве",
|
||||
"create-recipe-from-an-image": "Направи рецепт на основи слике",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Креирајте рецепт отпремањем његове слике. Mealie ће покушати да издвоји текст из слике користећи АИ и од њега направи рецепт.",
|
||||
"crop-and-rotate-the-image": "Исеците и ротирајте слику тако да је видљив само текст и да је у исправној оријентацији.",
|
||||
"create-from-images": "Креирај из слика",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Skapa nytt recept från grunden.",
|
||||
"create-recipes": "Skapa recept",
|
||||
"import-with-zip": "Importera från .zip",
|
||||
"create-recipe-from-an-image": "Skapa recept från en bild",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Skapa ett recept genom att ladda upp en bild på det. Mealie kommer att försöka extrahera texten från bilden med hjälp av AI och skapa ett recept från det.",
|
||||
"crop-and-rotate-the-image": "Beskär och rotera bilden så att endast texten är synlig och den är åt rätt håll.",
|
||||
"create-from-images": "Skapa från bild",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Sıfırdan yeni bir tarif oluşturun.",
|
||||
"create-recipes": "Tarif Oluştur",
|
||||
"import-with-zip": ".zip ile içe aktar",
|
||||
"create-recipe-from-an-image": "Görüntüden yemek tarifi oluştur",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Bir görsel yükleyerek yemek tarifi oluşturun. Mealie, yapay zekâ kullanarak görseldeki bilgileri çekip yemek tarifini oluşturmaya çalışacaktır.",
|
||||
"crop-and-rotate-the-image": "Resmi sadece yazılar gözükecek şekilde kesin ve döndürün, ayrıca doğru yönde durduğuna emin olun.",
|
||||
"create-from-images": "Resimden Oluştur",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Створити новий рецепт з нуля.",
|
||||
"create-recipes": "Створити рецепти",
|
||||
"import-with-zip": "Імпорт з .zip",
|
||||
"create-recipe-from-an-image": "Створити рецепт з зображення",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Створити рецепт, завантаживши його зображення. Mealie спробує витягти текст з зображення за допомогою ШІ та створити рецепт з нього.",
|
||||
"crop-and-rotate-the-image": "Обріжте і поверніть зображення так, щоб було видно лише текст в правильній орієнтації.",
|
||||
"create-from-images": "Створити з зображень",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "Create a new recipe from scratch.",
|
||||
"create-recipes": "Create Recipes",
|
||||
"import-with-zip": "Import with .zip",
|
||||
"create-recipe-from-an-image": "Create Recipe from an Image",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.",
|
||||
"crop-and-rotate-the-image": "Crop and rotate the image so that only the text is visible, and it's in the correct orientation.",
|
||||
"create-from-images": "Create from Images",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "从头创建一个新食谱。",
|
||||
"create-recipes": "创建食谱",
|
||||
"import-with-zip": "使用 .zip 导入",
|
||||
"create-recipe-from-an-image": "用图片创建食谱",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "通过上传食谱文本的图片来创建一个食谱。Mealie将尝试使用人工智能从图像中提取文本,并从中创建一个新的食谱。",
|
||||
"crop-and-rotate-the-image": "裁剪并旋转图片,使仅文字可见且方向正确。",
|
||||
"create-from-images": "从图片创建",
|
||||
|
||||
@@ -628,7 +628,7 @@
|
||||
"create-recipe-description": "從頭開始建立新食譜。",
|
||||
"create-recipes": "建立食譜",
|
||||
"import-with-zip": "以 .zip 匯入",
|
||||
"create-recipe-from-an-image": "從圖片建立食譜",
|
||||
"create-recipe-from-images": "Create Recipe from Images",
|
||||
"create-recipe-from-an-image-description": "上傳食譜圖片來建立食譜,Mealie 將嘗試使用 AI 從圖片中擷取文字並建立食譜。",
|
||||
"crop-and-rotate-the-image": "請裁切並旋轉圖片,使其僅顯示文字且方向正確。",
|
||||
"create-from-images": "從圖片建立",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<v-form ref="domUrlForm" @submit.prevent="createRecipe">
|
||||
<div>
|
||||
<v-card-title class="headline">
|
||||
{{ $t("recipe.create-recipe-from-an-image") }}
|
||||
{{ $t("recipe.create-recipe-from-images") }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p>{{ $t("recipe.create-recipe-from-an-image-description") }}</p>
|
||||
|
||||
@@ -17,7 +17,7 @@ from sqlalchemy.orm import Session, load_only
|
||||
|
||||
from alembic import op
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel, RecipeIngredientModel
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
|
||||
@@ -8,8 +8,8 @@ from mealie.core import root_logger
|
||||
from mealie.db.models._model_base import SqlAlchemyBase
|
||||
from mealie.db.models.group.group import Group
|
||||
from mealie.db.models.household.shopping_list import ShoppingList, ShoppingListMultiPurposeLabel
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
|
||||
from mealie.db.models.recipe.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
from mealie.db.models.users.users import User
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from .group import *
|
||||
from .labels import *
|
||||
from .recipe import *
|
||||
from .server import *
|
||||
from .users import *
|
||||
|
||||
23
mealie/db/models/_filterable_column.py
Normal file
23
mealie/db/models/_filterable_column.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
||||
class _FilterableColumn[T]:
|
||||
"""
|
||||
Drop-in replacement for `Mapped[]` that marks a column as filterable.
|
||||
Filterable columns can be used in query filter expressions.
|
||||
|
||||
Only valid on scalar column fields. Using it on a relationship type (e.g. `list[Model]`).
|
||||
"""
|
||||
|
||||
def __class_getitem__(cls, item: type) -> type:
|
||||
return Mapped[Annotated[item, mapped_column(info={"filterable": True})]]
|
||||
|
||||
|
||||
# SQLAlchemy doesn't play nice with mypy when overriding Mapped, so
|
||||
# we use this awkward workaround to make mypy happy
|
||||
if TYPE_CHECKING:
|
||||
FilterableColumn = Mapped
|
||||
else:
|
||||
FilterableColumn = _FilterableColumn
|
||||
@@ -5,6 +5,7 @@ from sqlalchemy import Integer
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, declared_attr, mapped_column, synonym
|
||||
from text_unidecode import unidecode
|
||||
|
||||
from ._filterable_column import FilterableColumn
|
||||
from ._model_utils.datetime import NaiveDateTime, get_utc_now
|
||||
|
||||
# Punctuation characters replaced with spaces during text normalization.
|
||||
@@ -16,8 +17,10 @@ _NORMALIZE_PUNCTUATION_TABLE = str.maketrans(NORMALIZE_PUNCTUATION, " " * len(NO
|
||||
|
||||
class SqlAlchemyBase(DeclarativeBase):
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
created_at: Mapped[datetime | None] = mapped_column(NaiveDateTime, default=get_utc_now, index=True)
|
||||
update_at: Mapped[datetime | None] = mapped_column(NaiveDateTime, default=get_utc_now, onupdate=get_utc_now)
|
||||
created_at: FilterableColumn[datetime | None] = mapped_column(NaiveDateTime, default=get_utc_now, index=True)
|
||||
update_at: FilterableColumn[datetime | None] = mapped_column(
|
||||
NaiveDateTime, default=get_utc_now, onupdate=get_utc_now
|
||||
)
|
||||
|
||||
@declared_attr
|
||||
def updated_at(cls) -> Mapped[datetime | None]:
|
||||
|
||||
@@ -5,9 +5,9 @@ import sqlalchemy.orm as orm
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.labels import MultiPurposeLabel
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
from ..household.cookbook import CookBook
|
||||
@@ -31,9 +31,9 @@ if TYPE_CHECKING:
|
||||
|
||||
class Group(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "groups"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name: Mapped[str] = mapped_column(sa.String, index=True, nullable=False, unique=True)
|
||||
slug: Mapped[str | None] = mapped_column(sa.String, index=True, unique=True)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name: FilterableColumn[str] = mapped_column(sa.String, index=True, nullable=False, unique=True)
|
||||
slug: FilterableColumn[str | None] = mapped_column(sa.String, index=True, unique=True)
|
||||
households: Mapped[list["Household"]] = orm.relationship("Household", back_populates="group")
|
||||
users: Mapped[list["User"]] = orm.relationship("User", back_populates="group")
|
||||
categories: Mapped[list[Category]] = orm.relationship(Category, secondary=group_to_categories, single_parent=True)
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
from sqlalchemy import Boolean, ForeignKey, Integer, String, UniqueConstraint, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
from .._model_utils import guid
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from ..recipe.category import Category, cookbooks_to_categories
|
||||
@@ -21,31 +21,31 @@ class CookBook(SqlAlchemyBase, BaseMixins):
|
||||
UniqueConstraint("slug", "group_id", name="cookbook_slug_group_id_key"),
|
||||
)
|
||||
|
||||
id: Mapped[guid.GUID] = mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
id: FilterableColumn[guid.GUID] = mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
|
||||
position: FilterableColumn[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
|
||||
group_id: Mapped[guid.GUID | None] = mapped_column(guid.GUID, ForeignKey("groups.id"), index=True)
|
||||
group_id: FilterableColumn[guid.GUID | None] = mapped_column(guid.GUID, ForeignKey("groups.id"), index=True)
|
||||
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="cookbooks")
|
||||
household_id: Mapped[guid.GUID | None] = mapped_column(guid.GUID, ForeignKey("households.id"), index=True)
|
||||
household_id: FilterableColumn[guid.GUID | None] = mapped_column(guid.GUID, ForeignKey("households.id"), index=True)
|
||||
household: Mapped[Optional["Household"]] = orm.relationship("Household", back_populates="cookbooks")
|
||||
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
slug: Mapped[str] = mapped_column(String, nullable=False, index=True)
|
||||
description: Mapped[str | None] = mapped_column(String, default="")
|
||||
public: Mapped[str | None] = mapped_column(Boolean, default=False)
|
||||
name: FilterableColumn[str] = mapped_column(String, nullable=False)
|
||||
slug: FilterableColumn[str] = mapped_column(String, nullable=False, index=True)
|
||||
description: FilterableColumn[str | None] = mapped_column(String, default="")
|
||||
public: FilterableColumn[str | None] = mapped_column(Boolean, default=False)
|
||||
query_filter_string: Mapped[str] = mapped_column(String, nullable=False, default="")
|
||||
|
||||
# Old filters - deprecated in favor of query filter strings
|
||||
categories: Mapped[list[Category]] = orm.relationship(
|
||||
Category, secondary=cookbooks_to_categories, single_parent=True
|
||||
)
|
||||
require_all_categories: Mapped[bool | None] = mapped_column(Boolean, default=True)
|
||||
require_all_categories: FilterableColumn[bool | None] = mapped_column(Boolean, default=True)
|
||||
|
||||
tags: Mapped[list[Tag]] = orm.relationship(Tag, secondary=cookbooks_to_tags, single_parent=True)
|
||||
require_all_tags: Mapped[bool | None] = mapped_column(Boolean, default=True)
|
||||
require_all_tags: FilterableColumn[bool | None] = mapped_column(Boolean, default=True)
|
||||
|
||||
tools: Mapped[list[Tool]] = orm.relationship(Tool, secondary=cookbooks_to_tools, single_parent=True)
|
||||
require_all_tools: Mapped[bool | None] = mapped_column(Boolean, default=True)
|
||||
require_all_tools: FilterableColumn[bool | None] = mapped_column(Boolean, default=True)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
|
||||
@@ -5,7 +5,7 @@ import sqlalchemy.orm as orm
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
from ..recipe.ingredient import households_to_ingredient_foods
|
||||
@@ -33,9 +33,9 @@ class Household(SqlAlchemyBase, BaseMixins):
|
||||
sa.UniqueConstraint("group_id", "slug", name="household_slug_group_id_key"),
|
||||
)
|
||||
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name: Mapped[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||
slug: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name: FilterableColumn[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||
slug: FilterableColumn[str | None] = mapped_column(sa.String, index=True)
|
||||
|
||||
invite_tokens: Mapped[list["GroupInviteToken"]] = orm.relationship(
|
||||
"GroupInviteToken", back_populates="household", cascade="all, delete-orphan"
|
||||
@@ -48,7 +48,7 @@ class Household(SqlAlchemyBase, BaseMixins):
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group_id: FilterableColumn[GUID] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="households")
|
||||
users: Mapped[list["User"]] = orm.relationship("User", back_populates="household")
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from mealie.db.models.recipe.tag import Tag, plan_rules_to_tags
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
from ..recipe.category import Category, plan_rules_to_categories
|
||||
@@ -30,14 +30,14 @@ plan_rules_to_households = Table(
|
||||
class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
|
||||
__tablename__ = "group_meal_plan_rules"
|
||||
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
household_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("households.id"), index=True)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
group_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
household_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("households.id"), index=True)
|
||||
|
||||
day: Mapped[str] = mapped_column(
|
||||
day: FilterableColumn[str] = mapped_column(
|
||||
String, nullable=False, default="unset"
|
||||
) # "MONDAY", "TUESDAY", "WEDNESDAY", etc...
|
||||
entry_type: Mapped[str] = mapped_column(
|
||||
entry_type: FilterableColumn[str] = mapped_column(
|
||||
String, nullable=False, default=""
|
||||
) # "breakfast", "lunch", "dinner", etc ...
|
||||
query_filter_string: Mapped[str] = mapped_column(String, nullable=False, default="")
|
||||
@@ -55,19 +55,19 @@ class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
|
||||
class GroupMealPlan(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "group_meal_plans"
|
||||
|
||||
date: Mapped[datetime.date] = mapped_column(Date, index=True, nullable=False)
|
||||
entry_type: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||
title: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||
text: Mapped[str] = mapped_column(String, nullable=False)
|
||||
date: FilterableColumn[datetime.date] = mapped_column(Date, index=True, nullable=False)
|
||||
entry_type: FilterableColumn[str] = mapped_column(String, index=True, nullable=False)
|
||||
title: FilterableColumn[str] = mapped_column(String, index=True, nullable=False)
|
||||
text: FilterableColumn[str] = mapped_column(String, nullable=False)
|
||||
|
||||
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), index=True)
|
||||
group_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), index=True)
|
||||
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="mealplans")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("user", "household_id")
|
||||
household: AssociationProxy["Household"] = association_proxy("user", "household")
|
||||
user_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("users.id"), index=True)
|
||||
user_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("users.id"), index=True)
|
||||
user: Mapped[Optional["User"]] = orm.relationship("User", back_populates="mealplans")
|
||||
|
||||
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
|
||||
recipe_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
|
||||
recipe: Mapped[Optional["RecipeModel"]] = orm.relationship(
|
||||
"RecipeModel", back_populates="meal_entries", uselist=False
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import sqlalchemy.orm as orm
|
||||
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
@@ -15,27 +15,29 @@ if TYPE_CHECKING:
|
||||
|
||||
class HouseholdPreferencesModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "household_preferences"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
household_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("households.id"), nullable=False, index=True)
|
||||
household_id: FilterableColumn[GUID | None] = mapped_column(
|
||||
GUID, sa.ForeignKey("households.id"), nullable=False, index=True
|
||||
)
|
||||
household: Mapped[Optional["Household"]] = orm.relationship("Household", back_populates="preferences")
|
||||
group_id: AssociationProxy[GUID] = association_proxy("household", "group_id")
|
||||
|
||||
private_household: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||
show_announcements: Mapped[bool] = mapped_column(sa.Boolean, default=True)
|
||||
private_household: FilterableColumn[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||
show_announcements: FilterableColumn[bool] = mapped_column(sa.Boolean, default=True)
|
||||
|
||||
lock_recipe_edits_from_other_households: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||
first_day_of_week: Mapped[int | None] = mapped_column(sa.Integer, default=0)
|
||||
lock_recipe_edits_from_other_households: FilterableColumn[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||
first_day_of_week: FilterableColumn[int | None] = mapped_column(sa.Integer, default=0)
|
||||
|
||||
# Recipe Defaults
|
||||
recipe_public: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||
recipe_show_nutrition: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
recipe_show_assets: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
recipe_landscape_view: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
recipe_disable_comments: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
recipe_public: FilterableColumn[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||
recipe_show_nutrition: FilterableColumn[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
recipe_show_assets: FilterableColumn[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
recipe_landscape_view: FilterableColumn[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
recipe_disable_comments: FilterableColumn[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
|
||||
# Deprecated
|
||||
recipe_disable_amount: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||
recipe_disable_amount: FilterableColumn[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
|
||||
@@ -8,10 +8,10 @@ from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.api_extras import ShoppingListExtras, ShoppingListItemExtras, api_extras
|
||||
from mealie.db.models.recipe.labels import MultiPurposeLabel
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
from ..recipe.ingredient import IngredientFoodModel, IngredientUnitModel
|
||||
@@ -25,18 +25,20 @@ if TYPE_CHECKING:
|
||||
|
||||
class ShoppingListItemRecipeReference(BaseMixins, SqlAlchemyBase):
|
||||
__tablename__ = "shopping_list_item_recipe_reference"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
shopping_list_item: Mapped["ShoppingListItem"] = orm.relationship(
|
||||
"ShoppingListItem", back_populates="recipe_references"
|
||||
)
|
||||
shopping_list_item_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("shopping_list_items.id"), primary_key=True)
|
||||
shopping_list_item_id: FilterableColumn[GUID] = mapped_column(
|
||||
GUID, ForeignKey("shopping_list_items.id"), primary_key=True
|
||||
)
|
||||
|
||||
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
|
||||
recipe_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
|
||||
recipe: Mapped[Optional["RecipeModel"]] = orm.relationship("RecipeModel", back_populates="shopping_list_item_refs")
|
||||
recipe_quantity: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
recipe_scale: Mapped[float] = mapped_column(Float, default=1)
|
||||
recipe_note: Mapped[str | None] = mapped_column(String)
|
||||
recipe_quantity: FilterableColumn[float] = mapped_column(Float, nullable=False)
|
||||
recipe_scale: FilterableColumn[float] = mapped_column(Float, default=1)
|
||||
recipe_note: FilterableColumn[str | None] = mapped_column(String)
|
||||
|
||||
group_id: AssociationProxy[GUID] = association_proxy("shopping_list_item", "group_id")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("shopping_list_item", "household_id")
|
||||
@@ -50,33 +52,33 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "shopping_list_items"
|
||||
|
||||
# Id's
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
shopping_list: Mapped["ShoppingList"] = orm.relationship("ShoppingList", back_populates="list_items")
|
||||
shopping_list_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("shopping_lists.id"), index=True)
|
||||
shopping_list_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("shopping_lists.id"), index=True)
|
||||
|
||||
group_id: AssociationProxy[GUID] = association_proxy("shopping_list", "group_id")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("shopping_list", "household_id")
|
||||
|
||||
# Meta
|
||||
is_ingredient: Mapped[bool | None] = mapped_column(Boolean, default=True)
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0, index=True)
|
||||
checked: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||
is_ingredient: FilterableColumn[bool | None] = mapped_column(Boolean, default=True)
|
||||
position: FilterableColumn[int] = mapped_column(Integer, nullable=False, default=0, index=True)
|
||||
checked: FilterableColumn[bool | None] = mapped_column(Boolean, default=False)
|
||||
|
||||
quantity: Mapped[float | None] = mapped_column(Float, default=1)
|
||||
note: Mapped[str | None] = mapped_column(String)
|
||||
quantity: FilterableColumn[float | None] = mapped_column(Float, default=1)
|
||||
note: FilterableColumn[str | None] = mapped_column(String)
|
||||
|
||||
extras: Mapped[list[ShoppingListItemExtras]] = orm.relationship(
|
||||
"ShoppingListItemExtras", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
# Scaling Items
|
||||
unit_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_units.id"))
|
||||
unit_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_units.id"))
|
||||
unit: Mapped[IngredientUnitModel | None] = orm.relationship(IngredientUnitModel, uselist=False)
|
||||
|
||||
food_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_foods.id"))
|
||||
food_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_foods.id"))
|
||||
food: Mapped[IngredientFoodModel | None] = orm.relationship(IngredientFoodModel, uselist=False)
|
||||
|
||||
label_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"))
|
||||
label_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"))
|
||||
label: Mapped[MultiPurposeLabel | None] = orm.relationship(
|
||||
MultiPurposeLabel, uselist=False, back_populates="shopping_list_items"
|
||||
)
|
||||
@@ -98,19 +100,19 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
class ShoppingListRecipeReference(BaseMixins, SqlAlchemyBase):
|
||||
__tablename__ = "shopping_list_recipe_reference"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
shopping_list: Mapped["ShoppingList"] = orm.relationship("ShoppingList", back_populates="recipe_references")
|
||||
shopping_list_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("shopping_lists.id"), primary_key=True)
|
||||
shopping_list_id: FilterableColumn[GUID] = mapped_column(GUID, ForeignKey("shopping_lists.id"), primary_key=True)
|
||||
group_id: AssociationProxy[GUID] = association_proxy("shopping_list", "group_id")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("shopping_list", "household_id")
|
||||
|
||||
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
|
||||
recipe_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
|
||||
recipe: Mapped[Optional["RecipeModel"]] = orm.relationship(
|
||||
"RecipeModel", uselist=False, back_populates="shopping_list_refs"
|
||||
)
|
||||
|
||||
recipe_quantity: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
recipe_quantity: FilterableColumn[float] = mapped_column(Float, nullable=False)
|
||||
model_config = ConfigDict(exclude={"id", "recipe"})
|
||||
|
||||
@auto_init()
|
||||
@@ -121,12 +123,12 @@ class ShoppingListRecipeReference(BaseMixins, SqlAlchemyBase):
|
||||
class ShoppingListMultiPurposeLabel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "shopping_lists_multi_purpose_labels"
|
||||
__table_args__ = (UniqueConstraint("shopping_list_id", "label_id", name="shopping_list_id_label_id_key"),)
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
shopping_list_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("shopping_lists.id"), primary_key=True)
|
||||
shopping_list_id: FilterableColumn[GUID] = mapped_column(GUID, ForeignKey("shopping_lists.id"), primary_key=True)
|
||||
shopping_list: Mapped["ShoppingList"] = orm.relationship("ShoppingList", back_populates="label_settings")
|
||||
|
||||
label_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"), primary_key=True)
|
||||
label_id: FilterableColumn[GUID] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"), primary_key=True)
|
||||
label: Mapped["MultiPurposeLabel"] = orm.relationship(
|
||||
"MultiPurposeLabel", back_populates="shopping_lists_label_settings"
|
||||
)
|
||||
@@ -134,7 +136,7 @@ class ShoppingListMultiPurposeLabel(SqlAlchemyBase, BaseMixins):
|
||||
group_id: AssociationProxy[GUID] = association_proxy("shopping_list", "group_id")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("shopping_list", "household_id")
|
||||
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
position: FilterableColumn[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
model_config = ConfigDict(exclude={"label"})
|
||||
|
||||
@auto_init()
|
||||
@@ -144,16 +146,16 @@ class ShoppingListMultiPurposeLabel(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
class ShoppingList(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "shopping_lists"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group_id: FilterableColumn[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="shopping_lists")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("user", "household_id")
|
||||
household: AssociationProxy["Household"] = association_proxy("user", "household")
|
||||
user_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("users.id"), nullable=False, index=True)
|
||||
user_id: FilterableColumn[GUID] = mapped_column(GUID, ForeignKey("users.id"), nullable=False, index=True)
|
||||
user: Mapped["User"] = orm.relationship("User", back_populates="shopping_lists")
|
||||
|
||||
name: Mapped[str | None] = mapped_column(String)
|
||||
name: FilterableColumn[str | None] = mapped_column(String)
|
||||
list_items: Mapped[list[ShoppingListItem]] = orm.relationship(
|
||||
ShoppingListItem,
|
||||
cascade="all, delete, delete-orphan",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.orm import mapped_column
|
||||
|
||||
from mealie.db.models._model_base import SqlAlchemyBase
|
||||
from mealie.db.models._model_base import FilterableColumn, SqlAlchemyBase
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ class ExtrasGeneric:
|
||||
This class is not an actual table, so it does not inherit from SqlAlchemyBase
|
||||
"""
|
||||
|
||||
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
|
||||
key_name: Mapped[str | None] = mapped_column(sa.String)
|
||||
value: Mapped[str | None] = mapped_column(sa.String)
|
||||
id: FilterableColumn[int] = mapped_column(sa.Integer, primary_key=True)
|
||||
key_name: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
value: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
|
||||
def __init__(self, key, value) -> None:
|
||||
self.key_name = key
|
||||
@@ -40,21 +40,25 @@ class ExtrasGeneric:
|
||||
# used specifically for recipe extras
|
||||
class ApiExtras(ExtrasGeneric, SqlAlchemyBase):
|
||||
__tablename__ = "api_extras"
|
||||
recipee_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"), index=True)
|
||||
recipee_id: FilterableColumn[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"), index=True)
|
||||
|
||||
|
||||
class IngredientFoodExtras(ExtrasGeneric, SqlAlchemyBase):
|
||||
__tablename__ = "ingredient_food_extras"
|
||||
ingredient_food_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("ingredient_foods.id"), index=True)
|
||||
ingredient_food_id: FilterableColumn[GUID | None] = mapped_column(
|
||||
GUID, sa.ForeignKey("ingredient_foods.id"), index=True
|
||||
)
|
||||
|
||||
|
||||
class ShoppingListExtras(ExtrasGeneric, SqlAlchemyBase):
|
||||
__tablename__ = "shopping_list_extras"
|
||||
shopping_list_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("shopping_lists.id"), index=True)
|
||||
shopping_list_id: FilterableColumn[GUID | None] = mapped_column(
|
||||
GUID, sa.ForeignKey("shopping_lists.id"), index=True
|
||||
)
|
||||
|
||||
|
||||
class ShoppingListItemExtras(ExtrasGeneric, SqlAlchemyBase):
|
||||
__tablename__ = "shopping_list_item_extras"
|
||||
shopping_list_item_id: Mapped[GUID | None] = mapped_column(
|
||||
shopping_list_item_id: FilterableColumn[GUID | None] = mapped_column(
|
||||
GUID, sa.ForeignKey("shopping_list_items.id"), index=True
|
||||
)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.orm import mapped_column
|
||||
|
||||
from mealie.db.models._model_base import SqlAlchemyBase
|
||||
from mealie.db.models._model_base import FilterableColumn, SqlAlchemyBase
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
|
||||
class RecipeAsset(SqlAlchemyBase):
|
||||
__tablename__ = "recipe_assets"
|
||||
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
|
||||
recipe_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"), index=True)
|
||||
name: Mapped[str | None] = mapped_column(sa.String)
|
||||
icon: Mapped[str | None] = mapped_column(sa.String)
|
||||
file_name: Mapped[str | None] = mapped_column(sa.String)
|
||||
id: FilterableColumn[int] = mapped_column(sa.Integer, primary_key=True)
|
||||
recipe_id: FilterableColumn[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"), index=True)
|
||||
name: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
icon: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
file_name: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
|
||||
def __init__(self, name=None, icon=None, file_name=None) -> None:
|
||||
self.name = name
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column, validates
|
||||
|
||||
from mealie.core import root_logger
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -54,12 +54,12 @@ class Category(SqlAlchemyBase, BaseMixins):
|
||||
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="category_slug_group_id_key"),)
|
||||
|
||||
# ID Relationships
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group_id: FilterableColumn[GUID] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="categories", foreign_keys=[group_id])
|
||||
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name: Mapped[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||
slug: Mapped[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name: FilterableColumn[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||
slug: FilterableColumn[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||
recipes: Mapped[list["RecipeModel"]] = orm.relationship(
|
||||
"RecipeModel", secondary=recipes_to_categories, back_populates="recipe_category"
|
||||
)
|
||||
|
||||
@@ -6,9 +6,9 @@ from sqlalchemy import Boolean, Float, ForeignKey, Integer, String, event, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
from mealie.db.models.recipe.api_extras import IngredientFoodExtras, api_extras
|
||||
from mealie.db.models.recipe.labels import MultiPurposeLabel
|
||||
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
@@ -29,19 +29,19 @@ households_to_ingredient_foods = sa.Table(
|
||||
|
||||
class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "ingredient_units"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
# ID Relationships
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group_id: FilterableColumn[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="ingredient_units", foreign_keys=[group_id])
|
||||
|
||||
name: Mapped[str | None] = mapped_column(String)
|
||||
plural_name: Mapped[str | None] = mapped_column(String)
|
||||
description: Mapped[str | None] = mapped_column(String)
|
||||
abbreviation: Mapped[str | None] = mapped_column(String)
|
||||
plural_abbreviation: Mapped[str | None] = mapped_column(String)
|
||||
use_abbreviation: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||
fraction: Mapped[bool | None] = mapped_column(Boolean, default=True)
|
||||
name: FilterableColumn[str | None] = mapped_column(String)
|
||||
plural_name: FilterableColumn[str | None] = mapped_column(String)
|
||||
description: FilterableColumn[str | None] = mapped_column(String)
|
||||
abbreviation: FilterableColumn[str | None] = mapped_column(String)
|
||||
plural_abbreviation: FilterableColumn[str | None] = mapped_column(String)
|
||||
use_abbreviation: FilterableColumn[bool | None] = mapped_column(Boolean, default=False)
|
||||
fraction: FilterableColumn[bool | None] = mapped_column(Boolean, default=True)
|
||||
|
||||
ingredients: Mapped[list["RecipeIngredientModel"]] = orm.relationship(
|
||||
"RecipeIngredientModel", back_populates="unit"
|
||||
@@ -53,14 +53,14 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
||||
)
|
||||
|
||||
# Standardization
|
||||
standard_quantity: Mapped[float | None] = mapped_column(Float)
|
||||
standard_unit: Mapped[str | None] = mapped_column(String)
|
||||
standard_quantity: FilterableColumn[float | None] = mapped_column(Float)
|
||||
standard_unit: FilterableColumn[str | None] = mapped_column(String)
|
||||
|
||||
# Automatically updated by sqlalchemy event, do not write to this manually
|
||||
name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
plural_name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
abbreviation_normalized: Mapped[str | None] = mapped_column(String, index=True)
|
||||
plural_abbreviation_normalized: Mapped[str | None] = mapped_column(String, index=True)
|
||||
name_normalized: FilterableColumn[str | None] = mapped_column(sa.String, index=True)
|
||||
plural_name_normalized: FilterableColumn[str | None] = mapped_column(sa.String, index=True)
|
||||
abbreviation_normalized: FilterableColumn[str | None] = mapped_column(String, index=True)
|
||||
plural_abbreviation_normalized: FilterableColumn[str | None] = mapped_column(String, index=True)
|
||||
|
||||
@auto_init()
|
||||
def __init__(
|
||||
@@ -152,18 +152,18 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "ingredient_foods"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
# ID Relationships
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group_id: FilterableColumn[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="ingredient_foods", foreign_keys=[group_id])
|
||||
households_with_ingredient_food: Mapped[list["Household"]] = orm.relationship(
|
||||
"Household", secondary=households_to_ingredient_foods, back_populates="ingredient_foods_on_hand"
|
||||
)
|
||||
|
||||
name: Mapped[str | None] = mapped_column(String)
|
||||
plural_name: Mapped[str | None] = mapped_column(String)
|
||||
description: Mapped[str | None] = mapped_column(String)
|
||||
name: FilterableColumn[str | None] = mapped_column(String)
|
||||
plural_name: FilterableColumn[str | None] = mapped_column(String)
|
||||
description: FilterableColumn[str | None] = mapped_column(String)
|
||||
|
||||
ingredients: Mapped[list["RecipeIngredientModel"]] = orm.relationship(
|
||||
"RecipeIngredientModel", back_populates="food"
|
||||
@@ -175,12 +175,12 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
||||
)
|
||||
extras: Mapped[list[IngredientFoodExtras]] = orm.relationship("IngredientFoodExtras", cascade="all, delete-orphan")
|
||||
|
||||
label_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"), index=True)
|
||||
label_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"), index=True)
|
||||
label: Mapped[MultiPurposeLabel | None] = orm.relationship(MultiPurposeLabel, uselist=False, back_populates="foods")
|
||||
|
||||
# Automatically updated by sqlalchemy event, do not write to this manually
|
||||
name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
plural_name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
name_normalized: FilterableColumn[str | None] = mapped_column(sa.String, index=True)
|
||||
plural_name_normalized: FilterableColumn[str | None] = mapped_column(sa.String, index=True)
|
||||
|
||||
model_config = ConfigDict(
|
||||
exclude={
|
||||
@@ -261,15 +261,15 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
class IngredientUnitAliasModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "ingredient_units_aliases"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
unit_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("ingredient_units.id"), primary_key=True)
|
||||
unit_id: FilterableColumn[GUID] = mapped_column(GUID, ForeignKey("ingredient_units.id"), primary_key=True)
|
||||
unit: Mapped["IngredientUnitModel"] = orm.relationship("IngredientUnitModel", back_populates="aliases")
|
||||
|
||||
name: Mapped[str] = mapped_column(String)
|
||||
name: FilterableColumn[str] = mapped_column(String)
|
||||
|
||||
# Automatically updated by sqlalchemy event, do not write to this manually
|
||||
name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
name_normalized: FilterableColumn[str | None] = mapped_column(sa.String, index=True)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, session: Session, name: str, **_) -> None:
|
||||
@@ -302,15 +302,15 @@ class IngredientUnitAliasModel(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
class IngredientFoodAliasModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "ingredient_foods_aliases"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
food_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("ingredient_foods.id"), primary_key=True)
|
||||
food_id: FilterableColumn[GUID] = mapped_column(GUID, ForeignKey("ingredient_foods.id"), primary_key=True)
|
||||
food: Mapped["IngredientFoodModel"] = orm.relationship("IngredientFoodModel", back_populates="aliases")
|
||||
|
||||
name: Mapped[str] = mapped_column(String)
|
||||
name: FilterableColumn[str] = mapped_column(String)
|
||||
|
||||
# Automatically updated by sqlalchemy event, do not write to this manually
|
||||
name_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
name_normalized: FilterableColumn[str | None] = mapped_column(sa.String, index=True)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, session: Session, name: str, **_) -> None:
|
||||
@@ -343,34 +343,34 @@ class IngredientFoodAliasModel(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
class RecipeIngredientModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "recipes_ingredients"
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
position: Mapped[int | None] = mapped_column(Integer, index=True)
|
||||
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"))
|
||||
id: FilterableColumn[int] = mapped_column(Integer, primary_key=True)
|
||||
position: FilterableColumn[int | None] = mapped_column(Integer, index=True)
|
||||
recipe_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"))
|
||||
|
||||
title: Mapped[str | None] = mapped_column(String) # Section Header - Shows if Present
|
||||
note: Mapped[str | None] = mapped_column(String) # Force Show Text - Overrides Concat
|
||||
title: FilterableColumn[str | None] = mapped_column(String) # Section Header - Shows if Present
|
||||
note: FilterableColumn[str | None] = mapped_column(String) # Force Show Text - Overrides Concat
|
||||
|
||||
# Scaling Items
|
||||
unit_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_units.id"), index=True)
|
||||
unit_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_units.id"), index=True)
|
||||
unit: Mapped[IngredientUnitModel | None] = orm.relationship(IngredientUnitModel, uselist=False)
|
||||
|
||||
food_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_foods.id"), index=True)
|
||||
food_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("ingredient_foods.id"), index=True)
|
||||
food: Mapped[IngredientFoodModel | None] = orm.relationship(IngredientFoodModel, uselist=False)
|
||||
quantity: Mapped[float | None] = mapped_column(Float)
|
||||
quantity: FilterableColumn[float | None] = mapped_column(Float)
|
||||
|
||||
original_text: Mapped[str | None] = mapped_column(String)
|
||||
original_text: FilterableColumn[str | None] = mapped_column(String)
|
||||
|
||||
reference_id: Mapped[GUID | None] = mapped_column(GUID) # Reference Links
|
||||
reference_id: FilterableColumn[GUID | None] = mapped_column(GUID) # Reference Links
|
||||
|
||||
# Recipe Reference
|
||||
referenced_recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
|
||||
referenced_recipe_id: FilterableColumn[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
|
||||
referenced_recipe: Mapped["RecipeModel"] = orm.relationship(
|
||||
"RecipeModel", back_populates="referenced_ingredients", foreign_keys=[referenced_recipe_id]
|
||||
)
|
||||
|
||||
# Automatically updated by sqlalchemy event, do not write to this manually
|
||||
note_normalized: Mapped[str | None] = mapped_column(String, index=True)
|
||||
original_text_normalized: Mapped[str | None] = mapped_column(String, index=True)
|
||||
note_normalized: FilterableColumn[str | None] = mapped_column(String, index=True)
|
||||
original_text_normalized: FilterableColumn[str | None] = mapped_column(String, index=True)
|
||||
|
||||
@auto_init()
|
||||
def __init__(
|
||||
|
||||
@@ -3,26 +3,26 @@ from typing import TYPE_CHECKING
|
||||
from sqlalchemy import ForeignKey, String, UniqueConstraint, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
|
||||
from ._model_utils.auto_init import auto_init
|
||||
from ._model_utils.guid import GUID
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .group.group import Group
|
||||
from .household.shopping_list import ShoppingListItem, ShoppingListMultiPurposeLabel
|
||||
from .recipe import IngredientFoodModel
|
||||
from ..group.group import Group
|
||||
from ..household.shopping_list import ShoppingListItem, ShoppingListMultiPurposeLabel
|
||||
from . import IngredientFoodModel
|
||||
|
||||
|
||||
class MultiPurposeLabel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "multi_purpose_labels"
|
||||
__table_args__ = (UniqueConstraint("name", "group_id", name="multi_purpose_labels_name_group_id_key"),)
|
||||
|
||||
id: Mapped[GUID] = mapped_column(GUID, default=GUID.generate, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
color: Mapped[str] = mapped_column(String(10), nullable=False, default="")
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, default=GUID.generate, primary_key=True)
|
||||
name: FilterableColumn[str] = mapped_column(String(255), nullable=False)
|
||||
color: FilterableColumn[str] = mapped_column(String(10), nullable=False, default="")
|
||||
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group_id: FilterableColumn[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="labels")
|
||||
|
||||
shopping_list_items: Mapped["ShoppingListItem"] = orm.relationship("ShoppingListItem", back_populates="label")
|
||||
@@ -1,22 +1,22 @@
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.orm import mapped_column
|
||||
|
||||
from mealie.db.models._model_base import SqlAlchemyBase
|
||||
from mealie.db.models._model_base import FilterableColumn, SqlAlchemyBase
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
|
||||
class Nutrition(SqlAlchemyBase):
|
||||
__tablename__ = "recipe_nutrition"
|
||||
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
|
||||
recipe_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"), index=True)
|
||||
id: FilterableColumn[int] = mapped_column(sa.Integer, primary_key=True)
|
||||
recipe_id: FilterableColumn[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"), index=True)
|
||||
|
||||
calories: Mapped[str | None] = mapped_column(sa.String)
|
||||
carbohydrate_content: Mapped[str | None] = mapped_column(sa.String)
|
||||
cholesterol_content: Mapped[str | None] = mapped_column(sa.String)
|
||||
fat_content: Mapped[str | None] = mapped_column(sa.String)
|
||||
fiber_content: Mapped[str | None] = mapped_column(sa.String)
|
||||
protein_content: Mapped[str | None] = mapped_column(sa.String)
|
||||
saturated_fat_content: Mapped[str | None] = mapped_column(sa.String)
|
||||
calories: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
carbohydrate_content: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
cholesterol_content: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
fat_content: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
fiber_content: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
protein_content: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
saturated_fat_content: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
|
||||
# `serving_size` is not a scaling factor, but a per-serving volume or mass
|
||||
# according to schema.org. E.g., "2 L", "500 g", "5 cups", etc.
|
||||
@@ -28,10 +28,10 @@ class Nutrition(SqlAlchemyBase):
|
||||
#
|
||||
# serving_size: Mapped[str | None] = mapped_column(sa.String)
|
||||
|
||||
sodium_content: Mapped[str | None] = mapped_column(sa.String)
|
||||
sugar_content: Mapped[str | None] = mapped_column(sa.String)
|
||||
trans_fat_content: Mapped[str | None] = mapped_column(sa.String)
|
||||
unsaturated_fat_content: Mapped[str | None] = mapped_column(sa.String)
|
||||
sodium_content: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
sugar_content: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
trans_fat_content: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
unsaturated_fat_content: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -16,7 +16,7 @@ from mealie.db.models._model_utils.datetime import NaiveDateTime, get_utc_today
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
from mealie.db.models.recipe.ingredient import RecipeIngredientModel
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
from ..household.household_to_recipe import HouseholdToRecipe
|
||||
from ..users.user_to_recipe import UserToRecipe
|
||||
from .api_extras import ApiExtras, api_extras
|
||||
@@ -45,20 +45,20 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
sa.UniqueConstraint("slug", "group_id", name="recipe_slug_group_id_key"),
|
||||
)
|
||||
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
slug: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
slug: FilterableColumn[str | None] = mapped_column(sa.String, index=True)
|
||||
|
||||
# ID Relationships
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group_id: FilterableColumn[GUID] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="recipes", foreign_keys=[group_id])
|
||||
|
||||
household_id: AssociationProxy[GUID] = association_proxy("user", "household_id")
|
||||
household: AssociationProxy["Household"] = association_proxy("user", "household")
|
||||
|
||||
user_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("users.id", use_alter=True), index=True)
|
||||
user_id: FilterableColumn[GUID | None] = mapped_column(GUID, sa.ForeignKey("users.id", use_alter=True), index=True)
|
||||
user: Mapped["User"] = orm.relationship("User", uselist=False, foreign_keys=[user_id])
|
||||
|
||||
rating: Mapped[float | None] = mapped_column(sa.Float, index=True, nullable=True)
|
||||
rating: FilterableColumn[float | None] = mapped_column(sa.Float, index=True, nullable=True)
|
||||
rated_by: Mapped[list["User"]] = orm.relationship(
|
||||
"User",
|
||||
secondary=UserToRecipe.__tablename__,
|
||||
@@ -78,20 +78,20 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
)
|
||||
|
||||
# General Recipe Properties
|
||||
name: Mapped[str] = mapped_column(sa.String, nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(sa.String)
|
||||
name: FilterableColumn[str] = mapped_column(sa.String, nullable=False)
|
||||
description: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
|
||||
image: Mapped[str | None] = mapped_column(sa.String)
|
||||
image: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
|
||||
# Time Related Properties
|
||||
total_time: Mapped[str | None] = mapped_column(sa.String)
|
||||
prep_time: Mapped[str | None] = mapped_column(sa.String)
|
||||
perform_time: Mapped[str | None] = mapped_column(sa.String)
|
||||
cook_time: Mapped[str | None] = mapped_column(sa.String)
|
||||
total_time: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
prep_time: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
perform_time: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
cook_time: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
|
||||
recipe_yield: Mapped[str | None] = mapped_column(sa.String)
|
||||
recipe_yield_quantity: Mapped[float] = mapped_column(sa.Float, index=True, default=0)
|
||||
recipe_servings: Mapped[float] = mapped_column(sa.Float, index=True, default=0)
|
||||
recipe_yield: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
recipe_yield_quantity: FilterableColumn[float] = mapped_column(sa.Float, index=True, default=0)
|
||||
recipe_servings: FilterableColumn[float] = mapped_column(sa.Float, index=True, default=0)
|
||||
|
||||
assets: Mapped[list[RecipeAsset]] = orm.relationship("RecipeAsset", cascade="all, delete-orphan")
|
||||
nutrition: Mapped[Nutrition] = orm.relationship("Nutrition", uselist=False, cascade="all, delete-orphan")
|
||||
@@ -137,14 +137,14 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
)
|
||||
tags: Mapped[list["Tag"]] = orm.relationship("Tag", secondary=recipes_to_tags, back_populates="recipes")
|
||||
notes: Mapped[list[Note]] = orm.relationship("Note", cascade="all, delete-orphan")
|
||||
org_url: Mapped[str | None] = mapped_column(sa.String)
|
||||
org_url: FilterableColumn[str | None] = mapped_column(sa.String)
|
||||
extras: Mapped[list[ApiExtras]] = orm.relationship("ApiExtras", cascade="all, delete-orphan")
|
||||
|
||||
# Time Stamp Properties
|
||||
date_added: Mapped[date | None] = mapped_column(sa.Date, default=get_utc_today)
|
||||
date_updated: Mapped[datetime | None] = mapped_column(NaiveDateTime)
|
||||
date_added: FilterableColumn[date | None] = mapped_column(sa.Date, default=get_utc_today)
|
||||
date_updated: FilterableColumn[datetime | None] = mapped_column(NaiveDateTime)
|
||||
|
||||
last_made: Mapped[datetime | None] = mapped_column(NaiveDateTime)
|
||||
last_made: FilterableColumn[datetime | None] = mapped_column(NaiveDateTime)
|
||||
made_by: Mapped[list["Household"]] = orm.relationship(
|
||||
"Household", secondary=HouseholdToRecipe.__tablename__, back_populates="made_recipes"
|
||||
)
|
||||
@@ -162,8 +162,8 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
)
|
||||
|
||||
# Automatically updated by sqlalchemy event, do not write to this manually
|
||||
name_normalized: Mapped[str] = mapped_column(sa.String, nullable=False, index=True)
|
||||
description_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
name_normalized: FilterableColumn[str] = mapped_column(sa.String, nullable=False, index=True)
|
||||
description_normalized: FilterableColumn[str | None] = mapped_column(sa.String, index=True)
|
||||
model_config = ConfigDict(
|
||||
get_attr="slug",
|
||||
exclude={
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from mealie.db.models._model_utils.datetime import NaiveDateTime
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
@@ -18,29 +18,29 @@ if TYPE_CHECKING:
|
||||
|
||||
class RecipeTimelineEvent(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "recipe_timeline_events"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
# Parent Recipe
|
||||
recipe_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("recipes.id"), nullable=False, index=True)
|
||||
recipe_id: FilterableColumn[GUID] = mapped_column(GUID, ForeignKey("recipes.id"), nullable=False, index=True)
|
||||
recipe: Mapped["RecipeModel"] = relationship("RecipeModel", back_populates="timeline_events")
|
||||
|
||||
group_id: AssociationProxy[GUID] = association_proxy("recipe", "group_id")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("recipe", "household_id")
|
||||
|
||||
# Related User (Actor)
|
||||
user_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("users.id"), nullable=False, index=True)
|
||||
user_id: FilterableColumn[GUID] = mapped_column(GUID, ForeignKey("users.id"), nullable=False, index=True)
|
||||
user: Mapped["User"] = relationship(
|
||||
"User", back_populates="recipe_timeline_events", single_parent=True, foreign_keys=[user_id]
|
||||
)
|
||||
|
||||
# General Properties
|
||||
subject: Mapped[str] = mapped_column(String, nullable=False)
|
||||
message: Mapped[str | None] = mapped_column(String)
|
||||
event_type: Mapped[str | None] = mapped_column(String)
|
||||
image: Mapped[str | None] = mapped_column(String)
|
||||
subject: FilterableColumn[str] = mapped_column(String, nullable=False)
|
||||
message: FilterableColumn[str | None] = mapped_column(String)
|
||||
event_type: FilterableColumn[str | None] = mapped_column(String)
|
||||
image: FilterableColumn[str | None] = mapped_column(String)
|
||||
|
||||
# Timestamps
|
||||
timestamp: Mapped[datetime | None] = mapped_column(NaiveDateTime, index=True)
|
||||
timestamp: FilterableColumn[datetime | None] = mapped_column(NaiveDateTime, index=True)
|
||||
|
||||
@auto_init()
|
||||
def __init__(
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from mealie.db.models._model_base import SqlAlchemyBase
|
||||
from mealie.db.models._model_base import FilterableColumn, SqlAlchemyBase
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
|
||||
class RecipeSettings(SqlAlchemyBase):
|
||||
__tablename__ = "recipe_settings"
|
||||
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True)
|
||||
recipe_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"), index=True)
|
||||
public: Mapped[bool | None] = mapped_column(sa.Boolean)
|
||||
show_nutrition: Mapped[bool | None] = mapped_column(sa.Boolean)
|
||||
show_assets: Mapped[bool | None] = mapped_column(sa.Boolean)
|
||||
landscape_view: Mapped[bool | None] = mapped_column(sa.Boolean)
|
||||
disable_comments: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
locked: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
id: FilterableColumn[int] = mapped_column(sa.Integer, primary_key=True)
|
||||
recipe_id: FilterableColumn[GUID | None] = mapped_column(GUID, sa.ForeignKey("recipes.id"), index=True)
|
||||
public: FilterableColumn[bool | None] = mapped_column(sa.Boolean)
|
||||
show_nutrition: FilterableColumn[bool | None] = mapped_column(sa.Boolean)
|
||||
show_assets: FilterableColumn[bool | None] = mapped_column(sa.Boolean)
|
||||
landscape_view: FilterableColumn[bool | None] = mapped_column(sa.Boolean)
|
||||
disable_comments: FilterableColumn[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
locked: FilterableColumn[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
|
||||
# Deprecated
|
||||
disable_amount: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||
|
||||
@@ -3,9 +3,9 @@ from typing import TYPE_CHECKING
|
||||
from uuid import uuid4
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
from mealie.db.models._model_utils.auto_init import auto_init
|
||||
from mealie.db.models._model_utils.datetime import NaiveDateTime
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
@@ -22,12 +22,12 @@ class RecipeShareTokenModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "recipe_share_tokens"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=uuid4)
|
||||
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group_id: FilterableColumn[GUID] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||
|
||||
recipe_id: Mapped[GUID] = mapped_column(GUID, sa.ForeignKey("recipes.id"), nullable=False, index=True)
|
||||
recipe: Mapped["RecipeModel"] = sa.orm.relationship("RecipeModel", back_populates="share_tokens", uselist=False)
|
||||
recipe_id: FilterableColumn[GUID] = mapped_column(GUID, sa.ForeignKey("recipes.id"), nullable=False, index=True)
|
||||
recipe: Mapped["RecipeModel"] = relationship("RecipeModel", back_populates="share_tokens", uselist=False)
|
||||
|
||||
expires_at: Mapped[datetime] = mapped_column(NaiveDateTime, nullable=False)
|
||||
expires_at: FilterableColumn[datetime] = mapped_column(NaiveDateTime, nullable=False)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
|
||||
@@ -6,7 +6,7 @@ from slugify import slugify
|
||||
from sqlalchemy.orm import Mapped, mapped_column, validates
|
||||
|
||||
from mealie.core import root_logger
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
from mealie.db.models._model_utils import guid
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -44,14 +44,16 @@ cookbooks_to_tags = sa.Table(
|
||||
class Tag(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "tags"
|
||||
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="tags_slug_group_id_key"),)
|
||||
id: Mapped[guid.GUID] = mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
|
||||
id: FilterableColumn[guid.GUID] = mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
|
||||
|
||||
# ID Relationships
|
||||
group_id: Mapped[guid.GUID] = mapped_column(guid.GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group_id: FilterableColumn[guid.GUID] = mapped_column(
|
||||
guid.GUID, sa.ForeignKey("groups.id"), nullable=False, index=True
|
||||
)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="tags", foreign_keys=[group_id])
|
||||
|
||||
name: Mapped[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||
slug: Mapped[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||
name: FilterableColumn[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||
slug: FilterableColumn[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||
recipes: Mapped[list["RecipeModel"]] = orm.relationship(
|
||||
"RecipeModel", secondary=recipes_to_tags, back_populates="tags"
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ from slugify import slugify
|
||||
from sqlalchemy import Boolean, Column, ForeignKey, String, Table, UniqueConstraint, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
from mealie.db.models._model_utils.auto_init import auto_init
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
@@ -42,14 +42,14 @@ cookbooks_to_tools = Table(
|
||||
class Tool(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "tools"
|
||||
__table_args__ = (UniqueConstraint("slug", "group_id", name="tools_slug_group_id_key"),)
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
# ID Relationships
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group_id: FilterableColumn[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="tools", foreign_keys=[group_id])
|
||||
|
||||
name: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||
slug: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||
name: FilterableColumn[str] = mapped_column(String, index=True, nullable=False)
|
||||
slug: FilterableColumn[str] = mapped_column(String, index=True, nullable=False)
|
||||
|
||||
households_with_tool: Mapped[list["Household"]] = orm.relationship(
|
||||
"Household", secondary=households_to_tools, back_populates="tools_on_hand"
|
||||
|
||||
@@ -13,7 +13,7 @@ from mealie.db.models._model_utils.auto_init import auto_init
|
||||
from mealie.db.models._model_utils.datetime import NaiveDateTime
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_base import BaseMixins, FilterableColumn, SqlAlchemyBase
|
||||
from .user_to_recipe import UserToRecipe
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -50,18 +50,21 @@ class AuthMethod(enum.Enum):
|
||||
|
||||
class User(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "users"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
full_name: Mapped[str | None] = mapped_column(String, index=True)
|
||||
username: Mapped[str | None] = mapped_column(String, index=True, unique=True)
|
||||
|
||||
id: FilterableColumn[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
full_name: FilterableColumn[str | None] = mapped_column(String, index=True)
|
||||
username: FilterableColumn[str | None] = mapped_column(String, index=True, unique=True)
|
||||
email: Mapped[str | None] = mapped_column(String, unique=True, index=True)
|
||||
password: Mapped[str | None] = mapped_column(String)
|
||||
auth_method: Mapped[Enum[AuthMethod]] = mapped_column(Enum(AuthMethod), default=AuthMethod.MEALIE)
|
||||
admin: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||
advanced: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group_id: FilterableColumn[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="users")
|
||||
household_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("households.id"), nullable=True, index=True)
|
||||
household_id: FilterableColumn[GUID | None] = mapped_column(
|
||||
GUID, ForeignKey("households.id"), nullable=True, index=True
|
||||
)
|
||||
household: Mapped["Household"] = orm.relationship("Household", back_populates="users")
|
||||
|
||||
cache_key: Mapped[str | None] = mapped_column(String, default="1234")
|
||||
|
||||
@@ -25,10 +25,10 @@ from mealie.db.models.household.shopping_list import (
|
||||
ShoppingListRecipeReference,
|
||||
)
|
||||
from mealie.db.models.household.webhooks import GroupWebhooksModel
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.category import Category
|
||||
from mealie.db.models.recipe.comment import RecipeComment
|
||||
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
|
||||
from mealie.db.models.recipe.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
from mealie.db.models.recipe.recipe_timeline import RecipeTimelineEvent
|
||||
from mealie.db.models.recipe.shared import RecipeShareTokenModel
|
||||
|
||||
@@ -25,7 +25,7 @@ from mealie.schema.response.pagination import (
|
||||
RequestQuery,
|
||||
)
|
||||
from mealie.schema.response.query_search import SearchFilter
|
||||
from mealie.services.query_filter.builder import QueryFilterBuilder
|
||||
from mealie.services.query_filter.builder import NonFilterableValueError, QueryFilterBuilder
|
||||
|
||||
from ._utils import NOT_SET, NotSet
|
||||
|
||||
@@ -467,6 +467,12 @@ class RepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase]:
|
||||
query, order_attr, order_dir, request_query.order_by_null_position
|
||||
)
|
||||
|
||||
except NonFilterableValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f'Invalid order_by statement "{request_query.order_by}": {e.message}',
|
||||
) from e
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
||||
@@ -26,7 +26,7 @@ from mealie.schema.recipe.recipe import RecipePagination, RecipeSummary, create_
|
||||
from mealie.schema.recipe.recipe_ingredient import IngredientFood
|
||||
from mealie.schema.recipe.recipe_suggestion import RecipeSuggestionQuery, RecipeSuggestionResponseItem
|
||||
from mealie.schema.recipe.recipe_tool import RecipeToolOut
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
from mealie.schema.response.pagination import OrderByNullPosition, OrderDirection, PaginationQuery
|
||||
from mealie.services.query_filter.builder import QueryFilterBuilder
|
||||
|
||||
from ..db.models._model_base import SqlAlchemyBase
|
||||
@@ -92,6 +92,20 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
||||
)
|
||||
return sa.cast(effective_rating, sa.Float)
|
||||
|
||||
def add_order_attr_to_query(
|
||||
self,
|
||||
query: sa.Select,
|
||||
order_attr: orm.InstrumentedAttribute,
|
||||
order_dir: OrderDirection,
|
||||
order_by_null: OrderByNullPosition | None,
|
||||
) -> sa.Select:
|
||||
# Sort recipe names by their normalized (accent-folded, lowercased) form so that
|
||||
# accented and umlaut characters sort next to their base letter (e.g. "Über" near
|
||||
# "U") instead of after "Z" by raw code point. See GH #6853.
|
||||
if order_attr is RecipeModel.name:
|
||||
order_attr = RecipeModel.name_normalized
|
||||
return super().add_order_attr_to_query(query, order_attr, order_dir, order_by_null)
|
||||
|
||||
def create(self, document: Recipe) -> Recipe: # type: ignore
|
||||
max_retries = 10
|
||||
original_name: str = document.name # type: ignore
|
||||
|
||||
@@ -22,6 +22,17 @@ from .keywords import PlaceholderKeyword, RelationalKeyword
|
||||
from .operators import LogicalOperator, RelationalOperator
|
||||
|
||||
|
||||
class NonFilterableValueError(ValueError):
|
||||
"""Raised when trying to filter by an unfilterable field"""
|
||||
|
||||
def __init__(self, field: str):
|
||||
self.message = f"Cannot filter on {field}"
|
||||
super().__init__(self.message)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.message}"
|
||||
|
||||
|
||||
class QueryFilterJSONPart(MealieModel):
|
||||
left_parenthesis: str | None = None
|
||||
right_parenthesis: str | None = None
|
||||
@@ -202,7 +213,7 @@ class QueryFilterBuilder:
|
||||
@classmethod
|
||||
def get_model_and_model_attr_from_attr_string[Model: SqlAlchemyBase](
|
||||
cls, attr_string: str, model: type[Model], *, query: sa.Select | None = None
|
||||
) -> tuple[SqlAlchemyBase, InstrumentedAttribute, sa.Select | None]:
|
||||
) -> tuple[type[SqlAlchemyBase], InstrumentedAttribute, sa.Select | None]:
|
||||
"""
|
||||
Take an attribute string and traverse a database model and its relationships to get the desired
|
||||
model and model attribute. Optionally provide a query to apply the necessary table joins.
|
||||
@@ -222,7 +233,7 @@ class QueryFilterBuilder:
|
||||
if not attribute_chain:
|
||||
raise ValueError("invalid query string: attribute name cannot be empty")
|
||||
|
||||
current_model: SqlAlchemyBase = model # type: ignore
|
||||
current_model: type[SqlAlchemyBase] = model
|
||||
for i, attribute_link in enumerate(attribute_chain):
|
||||
try:
|
||||
model_attr = getattr(current_model, attribute_link)
|
||||
@@ -259,6 +270,9 @@ class QueryFilterBuilder:
|
||||
if model_attr is None:
|
||||
raise ValueError(f"invalid attribute string: '{attr_string}'")
|
||||
|
||||
if not getattr(model_attr, "info", {}).get("filterable"):
|
||||
raise NonFilterableValueError(model_attr)
|
||||
|
||||
return current_model, model_attr, query
|
||||
|
||||
@classmethod
|
||||
@@ -334,7 +348,7 @@ class QueryFilterBuilder:
|
||||
column_aliases = column_aliases or {}
|
||||
|
||||
# join tables and build model chain
|
||||
attr_model_map: dict[int, Any] = {}
|
||||
attr_map: dict[int, tuple[type[SqlAlchemyBase], InstrumentedAttribute]] = {}
|
||||
model_attr: InstrumentedAttribute
|
||||
for i, component in enumerate(self.filter_components):
|
||||
if not isinstance(component, QueryFilterBuilderComponent):
|
||||
@@ -343,7 +357,7 @@ class QueryFilterBuilder:
|
||||
nested_model, model_attr, query = self.get_model_and_model_attr_from_attr_string(
|
||||
component.attribute_name, model, query=query
|
||||
)
|
||||
attr_model_map[i] = nested_model
|
||||
attr_map[i] = (nested_model, model_attr)
|
||||
|
||||
# build query filter
|
||||
partial_group: list[sa.ColumnElement] = []
|
||||
@@ -367,9 +381,9 @@ class QueryFilterBuilder:
|
||||
|
||||
else:
|
||||
component = cast(QueryFilterBuilderComponent, component)
|
||||
base_attribute_name = component.attribute_name.split(".")[-1]
|
||||
model_attr = getattr(attr_model_map[i], base_attribute_name)
|
||||
nested_model, model_attr = attr_map[i]
|
||||
|
||||
base_attribute_name = component.attribute_name.split(".")[-1]
|
||||
if (column_alias := column_aliases.get(base_attribute_name)) is not None:
|
||||
model_attr = column_alias
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import pytest
|
||||
import sqlalchemy as sa
|
||||
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
from mealie.db.models.users.users import LongLiveToken, User
|
||||
from mealie.services.query_filter.builder import (
|
||||
LogicalOperator,
|
||||
NonFilterableValueError,
|
||||
QueryFilterBuilder,
|
||||
QueryFilterJSON,
|
||||
QueryFilterJSONPart,
|
||||
@@ -74,3 +80,80 @@ def test_query_filter_builder_json_uses_raw_value():
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FilterableColumn tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_non_filterable_field_user_password_raises():
|
||||
"""Filtering on User.password (plain Mapped, not FilterableColumn) should raise ValueError."""
|
||||
with pytest.raises(NonFilterableValueError):
|
||||
QueryFilterBuilder.get_model_and_model_attr_from_attr_string("password", User)
|
||||
|
||||
|
||||
def test_non_filterable_field_user_email_raises():
|
||||
"""Filtering on User.email (plain Mapped, not FilterableColumn) should raise ValueError."""
|
||||
with pytest.raises(NonFilterableValueError):
|
||||
QueryFilterBuilder.get_model_and_model_attr_from_attr_string("email", User)
|
||||
|
||||
|
||||
def test_non_filterable_field_long_live_token_raises():
|
||||
"""Filtering on LongLiveToken.token (plain Mapped, not FilterableColumn) should raise ValueError."""
|
||||
with pytest.raises(NonFilterableValueError):
|
||||
QueryFilterBuilder.get_model_and_model_attr_from_attr_string("token", LongLiveToken)
|
||||
|
||||
|
||||
def test_filterable_field_does_not_raise():
|
||||
"""Filtering on a FilterableColumn field should not raise."""
|
||||
model, attr, _ = QueryFilterBuilder.get_model_and_model_attr_from_attr_string("full_name", User)
|
||||
assert model is User
|
||||
assert attr is User.full_name
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Relationship traversal tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_deep_traversal_to_filterable_field_works():
|
||||
"""Traversing a relationship to a FilterableColumn field should succeed."""
|
||||
model, attr, _ = QueryFilterBuilder.get_model_and_model_attr_from_attr_string("user.full_name", RecipeModel)
|
||||
assert model is User
|
||||
assert attr is User.full_name
|
||||
|
||||
|
||||
def test_deep_traversal_to_non_filterable_field_raises():
|
||||
"""Traversing a relationship to a plain Mapped field should raise ValueError."""
|
||||
with pytest.raises(NonFilterableValueError):
|
||||
QueryFilterBuilder.get_model_and_model_attr_from_attr_string("user.email", RecipeModel)
|
||||
|
||||
|
||||
def test_deep_traversal_user_password_raises():
|
||||
"""Traversing RecipeModel.user.password should raise ValueError."""
|
||||
with pytest.raises(NonFilterableValueError):
|
||||
QueryFilterBuilder.get_model_and_model_attr_from_attr_string("user.password", RecipeModel)
|
||||
|
||||
|
||||
def test_filter_query_user_email_raises():
|
||||
"""filter_query on user.email should raise ValueError."""
|
||||
query = sa.select(RecipeModel)
|
||||
builder = QueryFilterBuilder('user.email = "test@example.com"')
|
||||
with pytest.raises(NonFilterableValueError):
|
||||
builder.filter_query(query, RecipeModel)
|
||||
|
||||
|
||||
def test_filter_query_user_password_raises():
|
||||
"""filter_query on user.password should raise ValueError."""
|
||||
query = sa.select(RecipeModel)
|
||||
builder = QueryFilterBuilder('user.password = "secret"')
|
||||
with pytest.raises(NonFilterableValueError):
|
||||
builder.filter_query(query, RecipeModel)
|
||||
|
||||
|
||||
def test_association_proxy_resolving_to_filterable_field_works():
|
||||
"""Single-hop association proxy (e.g. household_id) resolving to a FilterableColumn should succeed."""
|
||||
model, attr, _ = QueryFilterBuilder.get_model_and_model_attr_from_attr_string("household_id", RecipeModel)
|
||||
assert model is User
|
||||
assert attr is User.household_id
|
||||
|
||||
@@ -647,6 +647,40 @@ def test_order_by_last_made(unique_user: TestUser, h2_user: TestUser):
|
||||
assert [item.id for item in h2_query.items] == [recipe_2.id, recipe_1.id]
|
||||
|
||||
|
||||
def test_order_by_name_is_accent_folded(unique_user: TestUser):
|
||||
# Names chosen so that raw code-point sorting (the previous behavior) would place the
|
||||
# umlaut/accented entries after "Zebra". Accent-folded sorting must instead place them
|
||||
# next to their base letter: "ärtsoppa" near "a", "Über" near "u". See GH #6853.
|
||||
names = ["Zebra", "ärtsoppa", "Apple", "Über"]
|
||||
recipes = [
|
||||
unique_user.repos.recipes.create(Recipe(user_id=unique_user.user_id, group_id=unique_user.group_id, name=name))
|
||||
for name in names
|
||||
]
|
||||
recipe_ids = ", ".join(str(recipe.id) for recipe in recipes)
|
||||
|
||||
ascending = unique_user.repos.recipes.page_all(
|
||||
PaginationQuery(
|
||||
page=1,
|
||||
per_page=-1,
|
||||
order_by="name",
|
||||
order_direction=OrderDirection.asc,
|
||||
query_filter=f"id IN [{recipe_ids}]",
|
||||
)
|
||||
)
|
||||
assert [item.name for item in ascending.items] == ["Apple", "ärtsoppa", "Über", "Zebra"]
|
||||
|
||||
descending = unique_user.repos.recipes.page_all(
|
||||
PaginationQuery(
|
||||
page=1,
|
||||
per_page=-1,
|
||||
order_by="name",
|
||||
order_direction=OrderDirection.desc,
|
||||
query_filter=f"id IN [{recipe_ids}]",
|
||||
)
|
||||
)
|
||||
assert [item.name for item in descending.items] == ["Zebra", "Über", "ärtsoppa", "Apple"]
|
||||
|
||||
|
||||
def test_coalesce_last_made(unique_user: TestUser):
|
||||
dt = datetime.now(UTC)
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ from mealie.db.models.household.household import Household
|
||||
from mealie.db.models.household.household_to_recipe import HouseholdToRecipe
|
||||
from mealie.db.models.household.mealplan import GroupMealPlanRules
|
||||
from mealie.db.models.household.shopping_list import ShoppingList
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
|
||||
from mealie.db.models.recipe.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
from mealie.db.models.recipe.tool import Tool
|
||||
from mealie.db.models.users.user_to_recipe import UserToRecipe
|
||||
|
||||
Reference in New Issue
Block a user