mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-30 17:53:31 -04:00 
			
		
		
		
	Add Database Layer for Recipe Scaling (#506)
* move badge * fix add individual ingredient * fix redirect issue Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
		| @@ -31,6 +31,7 @@ const apiReq = { | |||||||
|   post: async function(url, data, getErrorText = defaultErrorText, getSuccessText) { |   post: async function(url, data, getErrorText = defaultErrorText, getSuccessText) { | ||||||
|     const response = await axios.post(url, data).catch(function(error) { |     const response = await axios.post(url, data).catch(function(error) { | ||||||
|       handleError(error, getErrorText); |       handleError(error, getErrorText); | ||||||
|  |       return error; | ||||||
|     }); |     }); | ||||||
|     return handleResponse(response, getSuccessText); |     return handleResponse(response, getSuccessText); | ||||||
|   }, |   }, | ||||||
| @@ -38,6 +39,7 @@ const apiReq = { | |||||||
|   put: async function(url, data, getErrorText = defaultErrorText, getSuccessText) { |   put: async function(url, data, getErrorText = defaultErrorText, getSuccessText) { | ||||||
|     const response = await axios.put(url, data).catch(function(error) { |     const response = await axios.put(url, data).catch(function(error) { | ||||||
|       handleError(error, getErrorText); |       handleError(error, getErrorText); | ||||||
|  |       return error; | ||||||
|     }); |     }); | ||||||
|     return handleResponse(response, getSuccessText); |     return handleResponse(response, getSuccessText); | ||||||
|   }, |   }, | ||||||
| @@ -45,6 +47,7 @@ const apiReq = { | |||||||
|   patch: async function(url, data, getErrorText = defaultErrorText, getSuccessText) { |   patch: async function(url, data, getErrorText = defaultErrorText, getSuccessText) { | ||||||
|     const response = await axios.patch(url, data).catch(function(error) { |     const response = await axios.patch(url, data).catch(function(error) { | ||||||
|       handleError(error, getErrorText); |       handleError(error, getErrorText); | ||||||
|  |       return error; | ||||||
|     }); |     }); | ||||||
|     return handleResponse(response, getSuccessText); |     return handleResponse(response, getSuccessText); | ||||||
|   }, |   }, | ||||||
| @@ -52,12 +55,14 @@ const apiReq = { | |||||||
|   get: async function(url, data, getErrorText = defaultErrorText) { |   get: async function(url, data, getErrorText = defaultErrorText) { | ||||||
|     return axios.get(url, data).catch(function(error) { |     return axios.get(url, data).catch(function(error) { | ||||||
|       handleError(error, getErrorText); |       handleError(error, getErrorText); | ||||||
|  |       return error; | ||||||
|     }); |     }); | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   delete: async function(url, data, getErrorText = defaultErrorText, getSuccessText = defaultSuccessText) { |   delete: async function(url, data, getErrorText = defaultErrorText, getSuccessText = defaultSuccessText) { | ||||||
|     const response = await axios.delete(url, data).catch(function(error) { |     const response = await axios.delete(url, data).catch(function(error) { | ||||||
|       handleError(error, getErrorText); |       handleError(error, getErrorText); | ||||||
|  |       return error; | ||||||
|     }); |     }); | ||||||
|     return handleResponse(response, getSuccessText); |     return handleResponse(response, getSuccessText); | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -35,9 +35,11 @@ export const recipeAPI = { | |||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   async requestDetails(recipeSlug) { |   async requestDetails(recipeSlug) { | ||||||
|     let response = await apiReq.get(API_ROUTES.recipesRecipeSlug(recipeSlug)); |     const response = await apiReq.get(API_ROUTES.recipesRecipeSlug(recipeSlug)); | ||||||
|     if (response && response.data) return response.data; |     if (response.response) { | ||||||
|     else return null; |       return response.response; | ||||||
|  |     } | ||||||
|  |     return response; | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   updateImage(recipeSlug, fileObject, overrideSuccessMsg = false) { |   updateImage(recipeSlug, fileObject, overrideSuccessMsg = false) { | ||||||
|   | |||||||
| @@ -16,26 +16,14 @@ | |||||||
|       :top="menuTop" |       :top="menuTop" | ||||||
|       :nudge-top="menuTop ? '5' : '0'" |       :nudge-top="menuTop ? '5' : '0'" | ||||||
|       allow-overflow |       allow-overflow | ||||||
|  |       close-delay="125" | ||||||
|  |       open-on-hover | ||||||
|     > |     > | ||||||
|       <template v-slot:activator="{ on: onMenu, attrs: attrsMenu }"> |       <template v-slot:activator="{ on, attrs }"> | ||||||
|         <v-tooltip bottom dark :color="color"> |         <v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent> | ||||||
|           <template v-slot:activator="{ on: onTooltip, attrs: attrsTooltip }"> |  | ||||||
|             <v-btn |  | ||||||
|               :fab="fab" |  | ||||||
|               :small="fab" |  | ||||||
|               :color="color" |  | ||||||
|               :icon="!fab" |  | ||||||
|               dark |  | ||||||
|               v-bind="{ ...attrsMenu, ...attrsTooltip }" |  | ||||||
|               v-on="{ ...onMenu, ...onTooltip }" |  | ||||||
|               @click.prevent |  | ||||||
|             > |  | ||||||
|           <v-icon>{{ menuIcon }}</v-icon> |           <v-icon>{{ menuIcon }}</v-icon> | ||||||
|         </v-btn> |         </v-btn> | ||||||
|       </template> |       </template> | ||||||
|           <span>{{ $t("general.more") }}</span> |  | ||||||
|         </v-tooltip> |  | ||||||
|       </template> |  | ||||||
|       <v-list dense> |       <v-list dense> | ||||||
|         <v-list-item |         <v-list-item | ||||||
|           v-for="(item, index) in loggedIn && cardMenu ? userMenu : defaultMenu" |           v-for="(item, index) in loggedIn && cardMenu ? userMenu : defaultMenu" | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
|   <v-tooltip right :color="buttonStyle ? 'primary' : 'secondary'"> |   <v-tooltip bottom nudge-right="50" :color="buttonStyle ? 'primary' : 'secondary'"> | ||||||
|     <template v-slot:activator="{ on, attrs }"> |     <template v-slot:activator="{ on, attrs }"> | ||||||
|       <v-btn |       <v-btn | ||||||
|         small |         small | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
|   <div> |   <div v-if="value && value.length > 0"> | ||||||
|     <h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2> |     <h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2> | ||||||
|     <div v-if="edit"> |     <div v-if="edit"> | ||||||
|       <draggable :value="value" @input="updateIndex" @start="drag = true" @end="drag = false" handle=".handle"> |       <draggable :value="value" @input="updateIndex" @start="drag = true" @end="drag = false" handle=".handle"> | ||||||
| @@ -9,7 +9,7 @@ | |||||||
|               <v-textarea |               <v-textarea | ||||||
|                 class="mr-2" |                 class="mr-2" | ||||||
|                 :label="$t('recipe.ingredient')" |                 :label="$t('recipe.ingredient')" | ||||||
|                 v-model="value[index]" |                 v-model="value[index].note" | ||||||
|                 mdi-move-resize |                 mdi-move-resize | ||||||
|                 auto-grow |                 auto-grow | ||||||
|                 solo |                 solo | ||||||
| @@ -45,7 +45,7 @@ | |||||||
|         <v-checkbox hide-details :value="checked[index]" class="pt-0 my-auto py-auto" color="secondary"> </v-checkbox> |         <v-checkbox hide-details :value="checked[index]" class="pt-0 my-auto py-auto" color="secondary"> </v-checkbox> | ||||||
|  |  | ||||||
|         <v-list-item-content> |         <v-list-item-content> | ||||||
|           <vue-markdown class="ma-0 pa-0 text-subtitle-1 dense-markdown" :source="ingredient"> </vue-markdown> |           <vue-markdown class="ma-0 pa-0 text-subtitle-1 dense-markdown" :source="ingredient.note"> </vue-markdown> | ||||||
|         </v-list-item-content> |         </v-list-item-content> | ||||||
|       </v-list-item> |       </v-list-item> | ||||||
|     </div> |     </div> | ||||||
| @@ -85,9 +85,26 @@ export default { | |||||||
|   methods: { |   methods: { | ||||||
|     addIngredient(ingredients = null) { |     addIngredient(ingredients = null) { | ||||||
|       if (ingredients.length) { |       if (ingredients.length) { | ||||||
|         this.value.push(...ingredients); |         const newIngredients = ingredients.map(x => { | ||||||
|  |           return { | ||||||
|  |             title: null, | ||||||
|  |             note: x, | ||||||
|  |             unit: null, | ||||||
|  |             food: null, | ||||||
|  |             disableAmount: true, | ||||||
|  |             quantity: 1, | ||||||
|  |           }; | ||||||
|  |         }); | ||||||
|  |         this.value.push(...newIngredients); | ||||||
|       } else { |       } else { | ||||||
|         this.value.push(""); |         this.value.push({ | ||||||
|  |           title: null, | ||||||
|  |           note: "", | ||||||
|  |           unit: null, | ||||||
|  |           food: null, | ||||||
|  |           disableAmount: true, | ||||||
|  |           quantity: 1, | ||||||
|  |         }); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     generateKey(item, index) { |     generateKey(item, index) { | ||||||
|   | |||||||
| @@ -192,8 +192,12 @@ export default { | |||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       this.recipeDetails = await api.recipes.requestDetails(this.currentRecipe); |       const response = await api.recipes.requestDetails(this.currentRecipe); | ||||||
|       if (!this.recipeDetails) router.push(`/login`); |       console.log("View Response", { response }); | ||||||
|  |       if (response.status === 401) router.push(`/login`); | ||||||
|  |       if (response.status === 404) return; | ||||||
|  |  | ||||||
|  |       this.recipeDetails = response.data; | ||||||
|       this.skeleton = false; |       this.skeleton = false; | ||||||
|     }, |     }, | ||||||
|     getImage(slug) { |     getImage(slug) { | ||||||
|   | |||||||
| @@ -5,18 +5,19 @@ | |||||||
|         <v-icon left> |         <v-icon left> | ||||||
|           mdi-arrow-left-bold |           mdi-arrow-left-bold | ||||||
|         </v-icon> |         </v-icon> | ||||||
|         {{$t('shopping-list.all-lists')}} |         {{ $t("shopping-list.all-lists") }} | ||||||
|       </v-btn> |       </v-btn> | ||||||
|       <v-icon v-if="!list" large left> |       <v-icon v-if="!list" large left> | ||||||
|         mdi-format-list-checks |         mdi-format-list-checks | ||||||
|       </v-icon> |       </v-icon> | ||||||
|       <v-toolbar-title v-if="!list" class="headline"> {{$t('shopping-list.shopping-lists')}} </v-toolbar-title> |       <v-toolbar-title v-if="!list" class="headline"> {{ $t("shopping-list.shopping-lists") }} </v-toolbar-title> | ||||||
|       <v-spacer></v-spacer> |       <v-spacer></v-spacer> | ||||||
|       <BaseDialog |       <BaseDialog | ||||||
|         :title="$t('shopping-list.new-list')" |         :title="$t('shopping-list.new-list')" | ||||||
|         title-icon="mdi-format-list-checks" |         title-icon="mdi-format-list-checks" | ||||||
|         :submit-text="$t('general.create')" |         :submit-text="$t('general.create')" | ||||||
|         @submit="createNewList"> |         @submit="createNewList" | ||||||
|  |       > | ||||||
|         <template v-slot:open="{ open }"> |         <template v-slot:open="{ open }"> | ||||||
|           <TheButton create @click="open" /> |           <TheButton create @click="open" /> | ||||||
|         </template> |         </template> | ||||||
| @@ -41,7 +42,7 @@ | |||||||
|                 <v-icon left> |                 <v-icon left> | ||||||
|                   mdi-cart-check |                   mdi-cart-check | ||||||
|                 </v-icon> |                 </v-icon> | ||||||
|                 {{$t('general.view')}} |                 {{ $t("general.view") }} | ||||||
|               </v-btn> |               </v-btn> | ||||||
|             </v-card-actions> |             </v-card-actions> | ||||||
|           </v-card> |           </v-card> | ||||||
| @@ -65,7 +66,7 @@ | |||||||
|         <v-card-text> |         <v-card-text> | ||||||
|           <v-row dense v-for="(item, index) in activeList.items" :key="index"> |           <v-row dense v-for="(item, index) in activeList.items" :key="index"> | ||||||
|             <v-col v-if="edit" cols="12" class="d-flex no-wrap align-center"> |             <v-col v-if="edit" cols="12" class="d-flex no-wrap align-center"> | ||||||
|               <p class="mb-0">{{$t('shopping-list.quantity', [item.quantity])}}</p> |               <p class="mb-0">{{ $t("shopping-list.quantity", [item.quantity]) }}</p> | ||||||
|               <div v-if="edit"> |               <div v-if="edit"> | ||||||
|                 <v-btn x-small text class="ml-1" @click="activeList.items[index].quantity -= 1"> |                 <v-btn x-small text class="ml-1" @click="activeList.items[index].quantity -= 1"> | ||||||
|                   <v-icon> |                   <v-icon> | ||||||
| @@ -123,13 +124,13 @@ | |||||||
|             <v-icon left> |             <v-icon left> | ||||||
|               {{ $globals.icons.primary }} |               {{ $globals.icons.primary }} | ||||||
|             </v-icon> |             </v-icon> | ||||||
|             {{$t('shopping-list.from-recipe')}} |             {{ $t("shopping-list.from-recipe") }} | ||||||
|           </v-btn> |           </v-btn> | ||||||
|           <v-btn v-if="edit" color="success" @click="newItem"> |           <v-btn v-if="edit" color="success" @click="newItem"> | ||||||
|             <v-icon left> |             <v-icon left> | ||||||
|               {{ $globals.icons.create }} |               {{ $globals.icons.create }} | ||||||
|             </v-icon> |             </v-icon> | ||||||
|             {{$t('general.new')}} |             {{ $t("general.new") }} | ||||||
|           </v-btn> |           </v-btn> | ||||||
|         </v-card-actions> |         </v-card-actions> | ||||||
|       </v-card> |       </v-card> | ||||||
| @@ -197,7 +198,8 @@ export default { | |||||||
|       this.$refs.searchRecipe.open(); |       this.$refs.searchRecipe.open(); | ||||||
|     }, |     }, | ||||||
|     async importIngredients(selected) { |     async importIngredients(selected) { | ||||||
|       const recipe = await api.recipes.requestDetails(selected.slug); |       const response = await api.recipes.requestDetails(selected.slug); | ||||||
|  |       const recipe = response.data; | ||||||
|  |  | ||||||
|       const ingredients = recipe.recipeIngredient.map(x => ({ |       const ingredients = recipe.recipeIngredient.map(x => ({ | ||||||
|         title: "", |         title: "", | ||||||
|   | |||||||
| @@ -26,7 +26,8 @@ export const recipeRoutes = [ | |||||||
|     component: ViewRecipe, |     component: ViewRecipe, | ||||||
|     meta: { |     meta: { | ||||||
|       title: async route => { |       title: async route => { | ||||||
|         const recipe = await api.recipes.requestDetails(route.params.recipe); |         const response = await api.recipes.requestDetails(route.params.recipe); | ||||||
|  |         const recipe = response.data; | ||||||
|         if (recipe && recipe.name) return recipe.name; |         if (recipe && recipe.name) return recipe.name; | ||||||
|         else return null; |         else return null; | ||||||
|       }, |       }, | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								makefile
									
									
									
									
									
								
							| @@ -23,7 +23,7 @@ BROWSER := python -c "$$BROWSER_PYSCRIPT" | |||||||
| help: | help: | ||||||
| 	@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) | 	@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) | ||||||
|  |  | ||||||
| clean-purge: clean ## ⚠️  Removes All Developer Data for a fresh server start | purge: clean ## ⚠️  Removes All Developer Data for a fresh server start | ||||||
| 	rm -r ./dev/data/recipes/ | 	rm -r ./dev/data/recipes/ | ||||||
| 	rm -r ./dev/data/users/ | 	rm -r ./dev/data/users/ | ||||||
| 	rm -f ./dev/data/mealie_v*.db | 	rm -f ./dev/data/mealie_v*.db | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ from mealie.db.models.event import Event, EventNotification | |||||||
| from mealie.db.models.group import Group | from mealie.db.models.group import Group | ||||||
| from mealie.db.models.mealplan import MealPlan | from mealie.db.models.mealplan import MealPlan | ||||||
| from mealie.db.models.recipe.comment import RecipeComment | from mealie.db.models.recipe.comment import RecipeComment | ||||||
|  | from mealie.db.models.recipe.ingredient import IngredientFood, IngredientUnit | ||||||
| from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag | from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag | ||||||
| from mealie.db.models.recipe.settings import RecipeSettings | from mealie.db.models.recipe.settings import RecipeSettings | ||||||
| from mealie.db.models.settings import CustomPage, SiteSettings | from mealie.db.models.settings import CustomPage, SiteSettings | ||||||
| @@ -18,7 +19,8 @@ from mealie.schema.comments import CommentOut | |||||||
| from mealie.schema.event_notifications import EventNotificationIn | from mealie.schema.event_notifications import EventNotificationIn | ||||||
| from mealie.schema.events import Event as EventSchema | from mealie.schema.events import Event as EventSchema | ||||||
| from mealie.schema.meal import MealPlanOut | from mealie.schema.meal import MealPlanOut | ||||||
| from mealie.schema.recipe import Recipe | from mealie.schema.recipe import (Recipe, RecipeIngredientFood, | ||||||
|  |                                   RecipeIngredientUnit) | ||||||
| from mealie.schema.settings import CustomPageOut | from mealie.schema.settings import CustomPageOut | ||||||
| from mealie.schema.settings import SiteSettings as SiteSettingsSchema | from mealie.schema.settings import SiteSettings as SiteSettingsSchema | ||||||
| from mealie.schema.shopping_list import ShoppingListOut | from mealie.schema.shopping_list import ShoppingListOut | ||||||
| @@ -87,6 +89,20 @@ class _Recipes(BaseDocument): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class _IngredientFoods(BaseDocument): | ||||||
|  |     def __init__(self) -> None: | ||||||
|  |         self.primary_key = "id" | ||||||
|  |         self.sql_model = IngredientFood | ||||||
|  |         self.schema = RecipeIngredientFood | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class _IngredientUnits(BaseDocument): | ||||||
|  |     def __init__(self) -> None: | ||||||
|  |         self.primary_key = "id" | ||||||
|  |         self.sql_model = IngredientUnit | ||||||
|  |         self.schema = RecipeIngredientUnit | ||||||
|  |  | ||||||
|  |  | ||||||
| class _Categories(BaseDocument): | class _Categories(BaseDocument): | ||||||
|     def __init__(self) -> None: |     def __init__(self) -> None: | ||||||
|         self.primary_key = "slug" |         self.primary_key = "slug" | ||||||
| @@ -215,21 +231,28 @@ class _EventNotification(BaseDocument): | |||||||
|  |  | ||||||
| class Database: | class Database: | ||||||
|     def __init__(self) -> None: |     def __init__(self) -> None: | ||||||
|  |         # Recipes | ||||||
|         self.recipes = _Recipes() |         self.recipes = _Recipes() | ||||||
|         self.meals = _Meals() |         self.ingredient_foods = _IngredientUnits() | ||||||
|         self.settings = _Settings() |         self.ingredient_units = _IngredientFoods() | ||||||
|         self.themes = _Themes() |  | ||||||
|         self.categories = _Categories() |         self.categories = _Categories() | ||||||
|         self.tags = _Tags() |         self.tags = _Tags() | ||||||
|  |         self.comments = _Comments() | ||||||
|  |  | ||||||
|  |         # Site | ||||||
|  |         self.settings = _Settings() | ||||||
|  |         self.themes = _Themes() | ||||||
|  |         self.sign_ups = _SignUps() | ||||||
|  |         self.custom_pages = _CustomPages() | ||||||
|  |         self.event_notifications = _EventNotification() | ||||||
|  |         self.events = _Events() | ||||||
|  |  | ||||||
|  |         # Users / Groups | ||||||
|         self.users = _Users() |         self.users = _Users() | ||||||
|         self.api_tokens = _LongLiveToken() |         self.api_tokens = _LongLiveToken() | ||||||
|         self.sign_ups = _SignUps() |  | ||||||
|         self.groups = _Groups() |         self.groups = _Groups() | ||||||
|         self.custom_pages = _CustomPages() |         self.meals = _Meals() | ||||||
|         self.events = _Events() |  | ||||||
|         self.event_notifications = _EventNotification() |  | ||||||
|         self.shopping_lists = _ShoppingList() |         self.shopping_lists = _ShoppingList() | ||||||
|         self.comments = _Comments() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| db = Database() | db = Database() | ||||||
|   | |||||||
| @@ -1,5 +1,70 @@ | |||||||
| from mealie.db.models.model_base import SqlAlchemyBase | from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase | ||||||
| from sqlalchemy import Column, ForeignKey, Integer, String | from requests import Session | ||||||
|  | from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Table, orm | ||||||
|  |  | ||||||
|  | ingredients_to_units = Table( | ||||||
|  |     "ingredients_to_units", | ||||||
|  |     SqlAlchemyBase.metadata, | ||||||
|  |     Column("ingredient_units.id", Integer, ForeignKey("ingredient_units.id")), | ||||||
|  |     Column("recipes_ingredients_id", Integer, ForeignKey("recipes_ingredients.id")), | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | ingredients_to_foods = Table( | ||||||
|  |     "ingredients_to_foods", | ||||||
|  |     SqlAlchemyBase.metadata, | ||||||
|  |     Column("ingredient_foods.id", Integer, ForeignKey("ingredient_foods.id")), | ||||||
|  |     Column("recipes_ingredients_id", Integer, ForeignKey("recipes_ingredients.id")), | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class IngredientUnit(SqlAlchemyBase, BaseMixins): | ||||||
|  |     __tablename__ = "ingredient_units" | ||||||
|  |     id = Column(Integer, primary_key=True) | ||||||
|  |     name = Column(String) | ||||||
|  |     description = Column(String) | ||||||
|  |     ingredients = orm.relationship("RecipeIngredient", secondary=ingredients_to_units, back_populates="unit") | ||||||
|  |  | ||||||
|  |     def __init__(self, name: str, description: str = None) -> None: | ||||||
|  |         self.name = name | ||||||
|  |         self.description = description | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def get_ref_or_create(cls, session: Session, obj: dict): | ||||||
|  |         # sourcery skip: flip-comparison | ||||||
|  |         if obj is None: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |         name = obj.get("name") | ||||||
|  |  | ||||||
|  |         unit = session.query(cls).filter("name" == name).one_or_none() | ||||||
|  |  | ||||||
|  |         if not unit: | ||||||
|  |             return cls(**obj) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class IngredientFood(SqlAlchemyBase, BaseMixins): | ||||||
|  |     __tablename__ = "ingredient_foods" | ||||||
|  |     id = Column(Integer, primary_key=True) | ||||||
|  |     name = Column(String) | ||||||
|  |     description = Column(String) | ||||||
|  |     ingredients = orm.relationship("RecipeIngredient", secondary=ingredients_to_foods, back_populates="food") | ||||||
|  |  | ||||||
|  |     def __init__(self, name: str, description: str = None) -> None: | ||||||
|  |         self.name = name | ||||||
|  |         self.description = description | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def get_ref_or_create(cls, session: Session, obj: dict): | ||||||
|  |         # sourcery skip: flip-comparison | ||||||
|  |         if obj is None: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |         name = obj.get("name") | ||||||
|  |  | ||||||
|  |         unit = session.query(cls).filter("name" == name).one_or_none() | ||||||
|  |  | ||||||
|  |         if not unit: | ||||||
|  |             return cls(**obj) | ||||||
|  |  | ||||||
|  |  | ||||||
| class RecipeIngredient(SqlAlchemyBase): | class RecipeIngredient(SqlAlchemyBase): | ||||||
| @@ -7,8 +72,24 @@ class RecipeIngredient(SqlAlchemyBase): | |||||||
|     id = Column(Integer, primary_key=True) |     id = Column(Integer, primary_key=True) | ||||||
|     position = Column(Integer) |     position = Column(Integer) | ||||||
|     parent_id = Column(Integer, ForeignKey("recipes.id")) |     parent_id = Column(Integer, ForeignKey("recipes.id")) | ||||||
|     # title = Column(String) |  | ||||||
|     ingredient = Column(String) |  | ||||||
|  |  | ||||||
|     def update(self, ingredient): |     title = Column(String)  # Section Header - Shows if Present | ||||||
|         self.ingredient = ingredient |     note = Column(String)  # Force Show Text - Overrides Concat | ||||||
|  |  | ||||||
|  |     # Scaling Items | ||||||
|  |     unit = orm.relationship(IngredientUnit, secondary=ingredients_to_units, uselist=False) | ||||||
|  |     food = orm.relationship(IngredientFood, secondary=ingredients_to_foods, uselist=False) | ||||||
|  |     quantity = Column(Integer) | ||||||
|  |  | ||||||
|  |     # Extras | ||||||
|  |     disable_amount = Column(Boolean, default=False) | ||||||
|  |  | ||||||
|  |     def __init__( | ||||||
|  |         self, title: str, note: str, unit: dict, food: dict, quantity: int, disable_amount: bool, session: Session, **_ | ||||||
|  |     ) -> None: | ||||||
|  |         self.title = title | ||||||
|  |         self.note = note | ||||||
|  |         self.unit = IngredientUnit.get_ref_or_create(session, unit) | ||||||
|  |         self.food = IngredientFood.get_ref_or_create(session, food) | ||||||
|  |         self.quantity = quantity | ||||||
|  |         self.disable_amount = disable_amount | ||||||
|   | |||||||
| @@ -117,7 +117,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): | |||||||
|         self.tools = [Tool(tool=x) for x in tools] if tools else [] |         self.tools = [Tool(tool=x) for x in tools] if tools else [] | ||||||
|  |  | ||||||
|         self.recipe_yield = recipe_yield |         self.recipe_yield = recipe_yield | ||||||
|         self.recipe_ingredient = [RecipeIngredient(ingredient=ingr) for ingr in recipe_ingredient] |         self.recipe_ingredient = [RecipeIngredient(**ingr, session=session) for ingr in recipe_ingredient] | ||||||
|         self.assets = [RecipeAsset(**a) for a in assets] |         self.assets = [RecipeAsset(**a) for a in assets] | ||||||
|         self.recipe_instructions = [ |         self.recipe_instructions = [ | ||||||
|             RecipeInstruction(text=instruc.get("text"), title=instruc.get("title"), type=instruc.get("@type", None)) |             RecipeInstruction(text=instruc.get("text"), title=instruc.get("title"), type=instruc.get("@type", None)) | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ def get_shopping_list( | |||||||
|                 logger.error("Recipe Not Found") |                 logger.error("Recipe Not Found") | ||||||
|  |  | ||||||
|     new_list = ShoppingListIn( |     new_list = ShoppingListIn( | ||||||
|         name="MealPlan Shopping List", group=current_user.group, items=[ListItem(text=t) for t in all_ingredients] |         name="MealPlan Shopping List", group=current_user.group, items=[ListItem(text=t.note) for t in all_ingredients] | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     created_list: ShoppingListOut = db.shopping_lists.create(session, new_list) |     created_list: ShoppingListOut = db.shopping_lists.create(session, new_list) | ||||||
|   | |||||||
| @@ -76,6 +76,9 @@ def get_recipe(recipe_slug: str, session: Session = Depends(generate_session), i | |||||||
|  |  | ||||||
|     recipe: Recipe = db.recipes.get(session, recipe_slug) |     recipe: Recipe = db.recipes.get(session, recipe_slug) | ||||||
|  |  | ||||||
|  |     if not recipe: | ||||||
|  |         raise HTTPException(status.HTTP_404_NOT_FOUND) | ||||||
|  |  | ||||||
|     if recipe.settings.public or is_user: |     if recipe.settings.public or is_user: | ||||||
|  |  | ||||||
|         return recipe |         return recipe | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								mealie/routes/unit_and_foods/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								mealie/routes/unit_and_foods/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | from fastapi import APIRouter | ||||||
|  |  | ||||||
|  | from . import food_routes, unit_routes | ||||||
|  |  | ||||||
|  | units_and_foods_router = APIRouter(tags=["Food and Units"]) | ||||||
|  |  | ||||||
|  | units_and_foods_router.include_router(food_routes.router) | ||||||
|  | units_and_foods_router.include_router(unit_routes.router) | ||||||
							
								
								
									
										34
									
								
								mealie/routes/unit_and_foods/food_routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								mealie/routes/unit_and_foods/food_routes.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | from fastapi import APIRouter, Depends | ||||||
|  | from mealie.core.root_logger import get_logger | ||||||
|  | from mealie.routes.deps import get_current_user | ||||||
|  |  | ||||||
|  | router = APIRouter(prefix="/api/foods", dependencies=[Depends(get_current_user)]) | ||||||
|  | logger = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router.post("") | ||||||
|  | async def create_food(): | ||||||
|  |     """ Create food in the Database """ | ||||||
|  |     # Create food | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router.get("/{id}") | ||||||
|  | async def get_food(): | ||||||
|  |     """ Get food from the Database """ | ||||||
|  |     # Get food | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router.put("/{id}") | ||||||
|  | async def update_food(): | ||||||
|  |     """ Update food in the Database """ | ||||||
|  |     # Update food | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router.delete("/{id}") | ||||||
|  | async def delete_food(): | ||||||
|  |     """ Delete food from the Database """ | ||||||
|  |     # Delete food | ||||||
|  |     pass | ||||||
							
								
								
									
										34
									
								
								mealie/routes/unit_and_foods/unit_routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								mealie/routes/unit_and_foods/unit_routes.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | from fastapi import APIRouter, Depends | ||||||
|  | from mealie.core.root_logger import get_logger | ||||||
|  | from mealie.routes.deps import get_current_user | ||||||
|  |  | ||||||
|  | router = APIRouter(prefix="/api/units", dependencies=[Depends(get_current_user)]) | ||||||
|  | logger = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router.post("") | ||||||
|  | async def create_food(): | ||||||
|  |     """ Create food in the Database """ | ||||||
|  |     # Create food | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router.get("/{id}") | ||||||
|  | async def get_food(): | ||||||
|  |     """ Get food from the Database """ | ||||||
|  |     # Get food | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router.put("/{id}") | ||||||
|  | async def update_food(): | ||||||
|  |     """ Update food in the Database """ | ||||||
|  |     # Update food | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router.delete("/{id}") | ||||||
|  | async def delete_food(): | ||||||
|  |     """ Delete food from the Database """ | ||||||
|  |     # Delete food | ||||||
|  |     pass | ||||||
| @@ -59,6 +59,30 @@ class Nutrition(CamelModel): | |||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RecipeIngredientFood(CamelModel): | ||||||
|  |     name: str = "" | ||||||
|  |     description: str = "" | ||||||
|  |  | ||||||
|  |     class Config: | ||||||
|  |         orm_mode = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RecipeIngredientUnit(RecipeIngredientFood): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RecipeIngredient(CamelModel): | ||||||
|  |     title: Optional[str] | ||||||
|  |     note: Optional[str] | ||||||
|  |     unit: Optional[RecipeIngredientUnit] | ||||||
|  |     food: Optional[RecipeIngredientFood] | ||||||
|  |     disable_amount: bool = True | ||||||
|  |     quantity: int = 1 | ||||||
|  |  | ||||||
|  |     class Config: | ||||||
|  |         orm_mode = True | ||||||
|  |  | ||||||
|  |  | ||||||
| class RecipeSummary(CamelModel): | class RecipeSummary(CamelModel): | ||||||
|     id: Optional[int] |     id: Optional[int] | ||||||
|     name: Optional[str] |     name: Optional[str] | ||||||
| @@ -87,7 +111,7 @@ class RecipeSummary(CamelModel): | |||||||
|  |  | ||||||
| class Recipe(RecipeSummary): | class Recipe(RecipeSummary): | ||||||
|     recipe_yield: Optional[str] |     recipe_yield: Optional[str] | ||||||
|     recipe_ingredient: Optional[list[str]] |     recipe_ingredient: Optional[list[RecipeIngredient]] | ||||||
|     recipe_instructions: Optional[list[RecipeStep]] |     recipe_instructions: Optional[list[RecipeStep]] | ||||||
|     nutrition: Optional[Nutrition] |     nutrition: Optional[Nutrition] | ||||||
|     tools: Optional[list[str]] = [] |     tools: Optional[list[str]] = [] | ||||||
| @@ -134,7 +158,7 @@ class Recipe(RecipeSummary): | |||||||
|         def getter_dict(_cls, name_orm: RecipeModel): |         def getter_dict(_cls, name_orm: RecipeModel): | ||||||
|             return { |             return { | ||||||
|                 **GetterDict(name_orm), |                 **GetterDict(name_orm), | ||||||
|                 "recipe_ingredient": [x.ingredient for x in name_orm.recipe_ingredient], |                 # "recipe_ingredient": [x.note for x in name_orm.recipe_ingredient], | ||||||
|                 "recipe_category": [x.name for x in name_orm.recipe_category], |                 "recipe_category": [x.name for x in name_orm.recipe_category], | ||||||
|                 "tags": [x.name for x in name_orm.tags], |                 "tags": [x.name for x in name_orm.tags], | ||||||
|                 "tools": [x.tool for x in name_orm.tools], |                 "tools": [x.tool for x in name_orm.tools], | ||||||
| @@ -179,6 +203,16 @@ class Recipe(RecipeSummary): | |||||||
|  |  | ||||||
|         return slug |         return slug | ||||||
|  |  | ||||||
|  |     @validator("recipe_ingredient", always=True, pre=True) | ||||||
|  |     def validate_ingredients(recipe_ingredient, values): | ||||||
|  |         if not recipe_ingredient or not isinstance(recipe_ingredient, list): | ||||||
|  |             return recipe_ingredient | ||||||
|  |  | ||||||
|  |         if all(isinstance(elem, str) for elem in recipe_ingredient): | ||||||
|  |             return [RecipeIngredient(note=x) for x in recipe_ingredient] | ||||||
|  |  | ||||||
|  |         return recipe_ingredient | ||||||
|  |  | ||||||
|  |  | ||||||
| class AllRecipeRequest(BaseModel): | class AllRecipeRequest(BaseModel): | ||||||
|     properties: list[str] |     properties: list[str] | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user