mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -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:
parent
6a5fd8e4f8
commit
568a1a0015
@ -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)
|
Loading…
x
Reference in New Issue
Block a user