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:
Hayden 2022-02-27 12:48:21 -09:00 committed by GitHub
parent 6a5fd8e4f8
commit 568a1a0015
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 112 additions and 18 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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.

View File

@ -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]

View File

@ -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]

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)