mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04:00 
			
		
		
		
	Fix/multiple bug fixes (#1015)
* test-case for #1011 * revert regressions for #1011 * update cache key on new image * lint * fix #1012 * typing * random_recipe fixture * remove delete button when no listeners are present * spacing * update copy to match settings value
This commit is contained in:
		| @@ -21,7 +21,13 @@ | |||||||
|           type="number" |           type="number" | ||||||
|           placeholder="Quantity" |           placeholder="Quantity" | ||||||
|         > |         > | ||||||
|           <v-icon slot="prepend" class="mr-n1" color="error" @click="$emit('delete')"> |           <v-icon | ||||||
|  |             v-if="$listeners && $listeners.delete" | ||||||
|  |             slot="prepend" | ||||||
|  |             class="mr-n1" | ||||||
|  |             color="error" | ||||||
|  |             @click="$emit('delete')" | ||||||
|  |           > | ||||||
|             {{ $globals.icons.delete }} |             {{ $globals.icons.delete }} | ||||||
|           </v-icon> |           </v-icon> | ||||||
|         </v-text-field> |         </v-text-field> | ||||||
|   | |||||||
| @@ -180,7 +180,7 @@ | |||||||
| import draggable from "vuedraggable"; | import draggable from "vuedraggable"; | ||||||
| // @ts-ignore vue-markdown has no types | // @ts-ignore vue-markdown has no types | ||||||
| import VueMarkdown from "@adapttive/vue-markdown"; | import VueMarkdown from "@adapttive/vue-markdown"; | ||||||
| import { ref, toRefs, reactive, defineComponent, watch, onMounted } from "@nuxtjs/composition-api"; | import { ref, toRefs, reactive, defineComponent, watch, onMounted, watchEffect } from "@nuxtjs/composition-api"; | ||||||
| import { RecipeStep, IngredientReferences, RecipeIngredient } from "~/types/api-types/recipe"; | import { RecipeStep, IngredientReferences, RecipeIngredient } from "~/types/api-types/recipe"; | ||||||
| import { parseIngredientText } from "~/composables/recipes"; | import { parseIngredientText } from "~/composables/recipes"; | ||||||
| import { uuid4 } from "~/composables/use-utils"; | import { uuid4 } from "~/composables/use-utils"; | ||||||
| @@ -247,8 +247,9 @@ export default defineComponent({ | |||||||
|  |  | ||||||
|     // =============================================================== |     // =============================================================== | ||||||
|     // UI State Helpers |     // UI State Helpers | ||||||
|  |  | ||||||
|     function validateTitle(title: string | undefined) { |     function validateTitle(title: string | undefined) { | ||||||
|       return !(title === null || title === ""); |       return !(title === null || title === "" || title === undefined); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     watch(props.value, (v) => { |     watch(props.value, (v) => { | ||||||
| @@ -267,6 +268,8 @@ export default defineComponent({ | |||||||
|         if (element.id !== undefined) { |         if (element.id !== undefined) { | ||||||
|           showTitleEditor.value[element.id] = validateTitle(element.title); |           showTitleEditor.value[element.id] = validateTitle(element.title); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         showTitleEditor.value = { ...showTitleEditor.value }; | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
| @@ -283,17 +286,20 @@ export default defineComponent({ | |||||||
|         state.disabledSteps.push(stepIndex); |         state.disabledSteps.push(stepIndex); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function isChecked(stepIndex: number) { |     function isChecked(stepIndex: number) { | ||||||
|       if (state.disabledSteps.includes(stepIndex) && !props.edit) { |       if (state.disabledSteps.includes(stepIndex) && !props.edit) { | ||||||
|         return "disabled-card"; |         return "disabled-card"; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function toggleShowTitle(id: string) { |     function toggleShowTitle(id: string) { | ||||||
|       showTitleEditor.value[id] = !showTitleEditor.value[id]; |       showTitleEditor.value[id] = !showTitleEditor.value[id]; | ||||||
|  |  | ||||||
|       const temp = { ...showTitleEditor.value }; |       const temp = { ...showTitleEditor.value }; | ||||||
|       showTitleEditor.value = temp; |       showTitleEditor.value = temp; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function updateIndex(data: RecipeStep) { |     function updateIndex(data: RecipeStep) { | ||||||
|       context.emit("input", data); |       context.emit("input", data); | ||||||
|     } |     } | ||||||
| @@ -475,4 +481,3 @@ export default defineComponent({ | |||||||
|   background: none; |   background: none; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,15 +16,14 @@ | |||||||
|         confidence score is displayed on the right of the title item. This is an average of all scores and may not be |         confidence score is displayed on the right of the title item. This is an average of all scores and may not be | ||||||
|         wholey accurate. |         wholey accurate. | ||||||
|  |  | ||||||
|         <div class="mt-6"> |         <div class="my-4"> | ||||||
|           Alerts will be displayed if a matching foods or unit is found but does not exists in the database. |           Alerts will be displayed if a matching foods or unit is found but does not exists in the database. | ||||||
|         </div> |         </div> | ||||||
|         <v-divider class="my-4"> </v-divider> |         <div class="d-flex align-center mb-n4"> | ||||||
|         <div class="mb-n4"> |           <div class="mb-4">Select Parser</div> | ||||||
|           Select Parser |  | ||||||
|           <BaseOverflowButton |           <BaseOverflowButton | ||||||
|             v-model="parser" |             v-model="parser" | ||||||
|             btn-class="mx-2" |             btn-class="mx-2 mb-4" | ||||||
|             :items="[ |             :items="[ | ||||||
|               { |               { | ||||||
|                 text: 'Natural Language Processor ', |                 text: 'Natural Language Processor ', | ||||||
| @@ -270,4 +269,3 @@ export default defineComponent({ | |||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -60,13 +60,13 @@ | |||||||
|       <v-checkbox |       <v-checkbox | ||||||
|         v-model="group.preferences.recipeDisableComments" |         v-model="group.preferences.recipeDisableComments" | ||||||
|         class="mt-n4" |         class="mt-n4" | ||||||
|         label="Allow recipe comments from users in your group" |         label="Disable users from commenting on recipes" | ||||||
|         @change="groupActions.updatePreferences()" |         @change="groupActions.updatePreferences()" | ||||||
|       ></v-checkbox> |       ></v-checkbox> | ||||||
|       <v-checkbox |       <v-checkbox | ||||||
|         v-model="group.preferences.recipeDisableAmount" |         v-model="group.preferences.recipeDisableAmount" | ||||||
|         class="mt-n4" |         class="mt-n4" | ||||||
|         label="Enable organizing recipe ingredients by units and food" |         label="Disable organizing recipe ingredients by units and food" | ||||||
|         @change="groupActions.updatePreferences()" |         @change="groupActions.updatePreferences()" | ||||||
|       ></v-checkbox> |       ></v-checkbox> | ||||||
|     </section> |     </section> | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ def get_valid_call(func: Callable, args_dict) -> dict: | |||||||
|     return {k: v for k, v in args_dict.items() if k in valid_args} |     return {k: v for k, v in args_dict.items() if k in valid_args} | ||||||
|  |  | ||||||
|  |  | ||||||
| def safe_call(func, dict_args, **kwargs) -> Any: | def safe_call(func, dict_args: dict, **kwargs) -> Any: | ||||||
|     """ |     """ | ||||||
|     Safely calls the supplied function with the supplied dictionary of arguments. |     Safely calls the supplied function with the supplied dictionary of arguments. | ||||||
|     by removing any invalid arguments. |     by removing any invalid arguments. | ||||||
|   | |||||||
| @@ -33,5 +33,5 @@ class RecipeInstruction(SqlAlchemyBase): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|     @auto_init() |     @auto_init() | ||||||
|     def __init__(self, ingredient_references, **_) -> None: |     def __init__(self, ingredient_references, session, **_) -> None: | ||||||
|         self.ingredient_references = [RecipeIngredientRefLink(**ref) for ref in ingredient_references] |         self.ingredient_references = [RecipeIngredientRefLink(**ref, session=session) for ref in ingredient_references] | ||||||
|   | |||||||
| @@ -129,6 +129,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): | |||||||
|             "notes", |             "notes", | ||||||
|             "nutrition", |             "nutrition", | ||||||
|             "recipe_ingredient", |             "recipe_ingredient", | ||||||
|  |             "recipe_instructions", | ||||||
|             "settings", |             "settings", | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -146,10 +147,12 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): | |||||||
|         notes: list[dict] = None, |         notes: list[dict] = None, | ||||||
|         nutrition: dict = None, |         nutrition: dict = None, | ||||||
|         recipe_ingredient: list[dict] = None, |         recipe_ingredient: list[dict] = None, | ||||||
|  |         recipe_instructions: list[dict] = None, | ||||||
|         settings: dict = None, |         settings: dict = None, | ||||||
|         **_, |         **_, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         self.nutrition = Nutrition(**nutrition) if nutrition else Nutrition() |         self.nutrition = Nutrition(**nutrition) if nutrition else Nutrition() | ||||||
|  |         self.recipe_instructions = [RecipeInstruction(**step, session=session) for step in recipe_instructions] | ||||||
|         self.recipe_ingredient = [RecipeIngredient(**ingr, session=session) 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] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ from mealie.core import exceptions | |||||||
| from mealie.core.dependencies import temporary_zip_path | from mealie.core.dependencies import temporary_zip_path | ||||||
| from mealie.core.dependencies.dependencies import temporary_dir, validate_recipe_token | from mealie.core.dependencies.dependencies import temporary_dir, validate_recipe_token | ||||||
| from mealie.core.security import create_recipe_slug_token | from mealie.core.security import create_recipe_slug_token | ||||||
|  | from mealie.pkgs import cache | ||||||
| from mealie.repos.all_repositories import get_repositories | from mealie.repos.all_repositories import get_repositories | ||||||
| from mealie.repos.repository_recipes import RepositoryRecipes | from mealie.repos.repository_recipes import RepositoryRecipes | ||||||
| from mealie.routes._base import BaseUserController, controller | from mealie.routes._base import BaseUserController, controller | ||||||
| @@ -267,6 +268,9 @@ class RecipeController(BaseRecipeController): | |||||||
|         data_service = RecipeDataService(recipe.id) |         data_service = RecipeDataService(recipe.id) | ||||||
|         data_service.scrape_image(url.url) |         data_service.scrape_image(url.url) | ||||||
|  |  | ||||||
|  |         recipe.image = cache.cache_key.new_key() | ||||||
|  |         self.service.update_one(recipe.slug, recipe) | ||||||
|  |  | ||||||
|     @router.put("/{slug}/image", response_model=UpdateImageResponse, tags=["Recipe: Images and Assets"]) |     @router.put("/{slug}/image", response_model=UpdateImageResponse, tags=["Recipe: Images and Assets"]) | ||||||
|     def update_recipe_image(self, slug: str, image: bytes = File(...), extension: str = Form(...)): |     def update_recipe_image(self, slug: str, image: bytes = File(...), extension: str = Form(...)): | ||||||
|         recipe = self.mixins.get_one(slug) |         recipe = self.mixins.get_one(slug) | ||||||
| @@ -286,7 +290,7 @@ class RecipeController(BaseRecipeController): | |||||||
|         file: UploadFile = File(...), |         file: UploadFile = File(...), | ||||||
|     ): |     ): | ||||||
|         """Upload a file to store as a recipe asset""" |         """Upload a file to store as a recipe asset""" | ||||||
|         file_name = slugify(name) + "." + extension |         file_name = f"{slugify(name)}.{extension}" | ||||||
|         asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name) |         asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name) | ||||||
|  |  | ||||||
|         recipe = self.mixins.get_one(slug) |         recipe = self.mixins.get_one(slug) | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ from typing import Optional | |||||||
| from uuid import UUID, uuid4 | from uuid import UUID, uuid4 | ||||||
|  |  | ||||||
| from fastapi_camelcase import CamelModel | from fastapi_camelcase import CamelModel | ||||||
| from pydantic import Field | from pydantic import UUID4, Field | ||||||
|  |  | ||||||
|  |  | ||||||
| class IngredientReferences(CamelModel): | class IngredientReferences(CamelModel): | ||||||
| @@ -10,7 +10,7 @@ class IngredientReferences(CamelModel): | |||||||
|     A list of ingredient references. |     A list of ingredient references. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     reference_id: UUID = None |     reference_id: Optional[UUID4] | ||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								tests/fixtures/fixture_recipe.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										29
									
								
								tests/fixtures/fixture_recipe.py
									
									
									
									
										vendored
									
									
								
							| @@ -5,6 +5,7 @@ from mealie.repos.repository_factory import AllRepositories | |||||||
| from mealie.schema.recipe.recipe import Recipe, RecipeCategory | from mealie.schema.recipe.recipe import Recipe, RecipeCategory | ||||||
| from mealie.schema.recipe.recipe_category import CategorySave | from mealie.schema.recipe.recipe_category import CategorySave | ||||||
| from mealie.schema.recipe.recipe_ingredient import RecipeIngredient | from mealie.schema.recipe.recipe_ingredient import RecipeIngredient | ||||||
|  | from mealie.schema.recipe.recipe_step import RecipeStep | ||||||
| from tests.utils.factories import random_string | from tests.utils.factories import random_string | ||||||
| from tests.utils.fixture_schemas import TestUser | from tests.utils.fixture_schemas import TestUser | ||||||
| from tests.utils.recipe_data import get_raw_no_image, get_raw_recipe, get_recipe_test_cases | from tests.utils.recipe_data import get_raw_no_image, get_raw_recipe, get_recipe_test_cases | ||||||
| @@ -70,3 +71,31 @@ def recipe_categories(database: AllRepositories, unique_user: TestUser) -> list[ | |||||||
|             database.categories.delete(model.id) |             database.categories.delete(model.id) | ||||||
|         except sqlalchemy.exc.NoResultFound: |         except sqlalchemy.exc.NoResultFound: | ||||||
|             pass |             pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @fixture(scope="function") | ||||||
|  | def random_recipe(database: AllRepositories, unique_user: TestUser) -> Recipe: | ||||||
|  |     recipe = Recipe( | ||||||
|  |         user_id=unique_user.user_id, | ||||||
|  |         group_id=unique_user.group_id, | ||||||
|  |         name=random_string(10), | ||||||
|  |         recipe_ingredient=[ | ||||||
|  |             RecipeIngredient(note="Ingredient 1"), | ||||||
|  |             RecipeIngredient(note="Ingredient 2"), | ||||||
|  |             RecipeIngredient(note="Ingredient 3"), | ||||||
|  |         ], | ||||||
|  |         recipe_instructions=[ | ||||||
|  |             RecipeStep(text="Step 1"), | ||||||
|  |             RecipeStep(text="Step 2"), | ||||||
|  |             RecipeStep(text="Step 3"), | ||||||
|  |         ], | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     model = database.recipes.create(recipe) | ||||||
|  |  | ||||||
|  |     yield model | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         database.recipes.delete(model.slug) | ||||||
|  |     except sqlalchemy.exc.NoResultFound: | ||||||
|  |         pass | ||||||
|   | |||||||
| @@ -0,0 +1,49 @@ | |||||||
|  | import json | ||||||
|  | import random | ||||||
|  |  | ||||||
|  | from fastapi.testclient import TestClient | ||||||
|  |  | ||||||
|  | from mealie.schema.recipe.recipe import Recipe | ||||||
|  | from mealie.schema.recipe.recipe_step import IngredientReferences | ||||||
|  | from tests.utils import jsonify, routes | ||||||
|  | from tests.utils.fixture_schemas import TestUser | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_associate_ingredient_with_step(api_client: TestClient, unique_user: TestUser, random_recipe: Recipe): | ||||||
|  |     recipe: Recipe = random_recipe | ||||||
|  |  | ||||||
|  |     # Associate an ingredient with a step | ||||||
|  |  | ||||||
|  |     steps = {}  # key=step_id, value=ingredient_id | ||||||
|  |  | ||||||
|  |     for idx, step in enumerate(recipe.recipe_instructions): | ||||||
|  |         ingredients = random.choices(recipe.recipe_ingredient, k=2) | ||||||
|  |  | ||||||
|  |         step.ingredient_references = [ | ||||||
|  |             IngredientReferences(reference_id=ingredient.reference_id) for ingredient in ingredients | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         steps[idx] = [str(ingredient.reference_id) for ingredient in ingredients] | ||||||
|  |  | ||||||
|  |     response = api_client.put( | ||||||
|  |         routes.RoutesRecipe.item(recipe.slug), | ||||||
|  |         json=jsonify(recipe.dict()), | ||||||
|  |         headers=unique_user.token, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     assert response.status_code == 200 | ||||||
|  |  | ||||||
|  |     # Get Recipe and check that the ingredient is associated with the step | ||||||
|  |  | ||||||
|  |     response = api_client.get(routes.RoutesRecipe.item(recipe.slug), headers=unique_user.token) | ||||||
|  |  | ||||||
|  |     assert response.status_code == 200 | ||||||
|  |  | ||||||
|  |     recipe = json.loads(response.text) | ||||||
|  |  | ||||||
|  |     for idx, step in enumerate(recipe.get("recipeInstructions")): | ||||||
|  |         all_refs = [ref["referenceId"] for ref in step.get("ingredientReferences")] | ||||||
|  |  | ||||||
|  |         assert len(all_refs) == 2 | ||||||
|  |  | ||||||
|  |         assert all(ref in steps[idx] for ref in all_refs) | ||||||
		Reference in New Issue
	
	Block a user