mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
feat: select ingredients to add to shopping List (#2136)
* added recipe ingredient override to backend * pytest * new dialog to filter recipe items added to list
This commit is contained in:
parent
89b003589d
commit
5562effd66
@ -58,7 +58,6 @@
|
|||||||
readonly
|
readonly
|
||||||
v-on="on"
|
v-on="on"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<v-date-picker v-model="newMealdate" no-title @input="pickerMenu = false"></v-date-picker>
|
<v-date-picker v-model="newMealdate" no-title @input="pickerMenu = false"></v-date-picker>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
@ -77,7 +76,7 @@
|
|||||||
:key="list.id"
|
:key="list.id"
|
||||||
hover
|
hover
|
||||||
class="my-2 left-border"
|
class="my-2 left-border"
|
||||||
@click="addRecipeToList(list.id)"
|
@click="openShoppingListIngredientDialog(list)"
|
||||||
>
|
>
|
||||||
<v-card-title class="py-2">
|
<v-card-title class="py-2">
|
||||||
{{ list.name }}
|
{{ list.name }}
|
||||||
@ -85,6 +84,59 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
<BaseDialog
|
||||||
|
v-model="shoppingListIngredientDialog"
|
||||||
|
:title="selectedShoppingList ? selectedShoppingList.name : $t('recipe.add-to-list')"
|
||||||
|
:icon="$globals.icons.cartCheck"
|
||||||
|
width="70%"
|
||||||
|
:submit-text="$tc('recipe.add-to-list')"
|
||||||
|
@submit="addRecipeToList()"
|
||||||
|
>
|
||||||
|
<v-card
|
||||||
|
elevation="0"
|
||||||
|
height="fit-content"
|
||||||
|
max-height="60vh"
|
||||||
|
width="100%"
|
||||||
|
class="ingredient-grid"
|
||||||
|
:style="{ gridTemplateRows: `repeat(${Math.ceil(recipeIngredients.length / 2)}, min-content)` }"
|
||||||
|
style="overflow-y: auto"
|
||||||
|
>
|
||||||
|
<v-list-item
|
||||||
|
v-for="(ingredientData, i) in recipeIngredients"
|
||||||
|
:key="'ingredient' + i"
|
||||||
|
dense
|
||||||
|
@click="recipeIngredients[i].checked = !recipeIngredients[i].checked"
|
||||||
|
>
|
||||||
|
<v-checkbox
|
||||||
|
hide-details
|
||||||
|
:input-value="ingredientData.checked"
|
||||||
|
class="pt-0 my-auto py-auto"
|
||||||
|
color="secondary"
|
||||||
|
/>
|
||||||
|
<v-list-item-content :key="ingredientData.ingredient.quantity">
|
||||||
|
<SafeMarkdown class="ma-0 pa-0 text-subtitle-1 dense-markdown" :source="ingredientData.display" />
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-card>
|
||||||
|
<div class="d-flex justify-end mb-4 mt-2">
|
||||||
|
<BaseButtonGroup
|
||||||
|
:buttons="[
|
||||||
|
{
|
||||||
|
icon: $globals.icons.checkboxBlankOutline,
|
||||||
|
text: $tc('shopping-list.uncheck-all-items'),
|
||||||
|
event: 'uncheck',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.checkboxOutline,
|
||||||
|
text: $tc('shopping-list.check-all-items'),
|
||||||
|
event: 'check',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
@uncheck="bulkCheckIngredients(false)"
|
||||||
|
@check="bulkCheckIngredients(true)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
<v-menu
|
<v-menu
|
||||||
offset-y
|
offset-y
|
||||||
left
|
left
|
||||||
@ -121,7 +173,8 @@ import RecipeDialogShare from "./RecipeDialogShare.vue";
|
|||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
import { planTypeOptions } from "~/composables/use-group-mealplan";
|
import { planTypeOptions } from "~/composables/use-group-mealplan";
|
||||||
import { Recipe } from "~/lib/api/types/recipe";
|
import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
|
||||||
|
import { parseIngredientText } from "~/composables/recipes";
|
||||||
import { ShoppingListSummary } from "~/lib/api/types/group";
|
import { ShoppingListSummary } from "~/lib/api/types/group";
|
||||||
import { PlanEntryType } from "~/lib/api/types/meal-plan";
|
import { PlanEntryType } from "~/lib/api/types/meal-plan";
|
||||||
import { useAxiosDownloader } from "~/composables/api/use-axios-download";
|
import { useAxiosDownloader } from "~/composables/api/use-axios-download";
|
||||||
@ -232,6 +285,7 @@ export default defineComponent({
|
|||||||
recipeDeleteDialog: false,
|
recipeDeleteDialog: false,
|
||||||
mealplannerDialog: false,
|
mealplannerDialog: false,
|
||||||
shoppingListDialog: false,
|
shoppingListDialog: false,
|
||||||
|
shoppingListIngredientDialog: false,
|
||||||
recipeDuplicateDialog: false,
|
recipeDuplicateDialog: false,
|
||||||
recipeName: props.name,
|
recipeName: props.name,
|
||||||
loading: false,
|
loading: false,
|
||||||
@ -328,6 +382,9 @@ export default defineComponent({
|
|||||||
// Context Menu Event Handler
|
// Context Menu Event Handler
|
||||||
|
|
||||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||||
|
const selectedShoppingList = ref<ShoppingListSummary>();
|
||||||
|
const recipe = ref<Recipe>(props.recipe);
|
||||||
|
const recipeIngredients = ref<{ checked: boolean; ingredient: RecipeIngredient; display: string }[]>([]);
|
||||||
|
|
||||||
async function getShoppingLists() {
|
async function getShoppingLists() {
|
||||||
const { data } = await api.shopping.lists.getAll();
|
const { data } = await api.shopping.lists.getAll();
|
||||||
@ -336,11 +393,65 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addRecipeToList(listId: string) {
|
async function refreshRecipe() {
|
||||||
const { data } = await api.shopping.lists.addRecipe(listId, props.recipeId, props.recipeScale);
|
const { data } = await api.recipes.getOne(props.slug);
|
||||||
|
if (data) {
|
||||||
|
recipe.value = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openShoppingListIngredientDialog(list: ShoppingListSummary) {
|
||||||
|
selectedShoppingList.value = list;
|
||||||
|
if (!recipe.value) {
|
||||||
|
await refreshRecipe();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipe.value?.recipeIngredient) {
|
||||||
|
recipeIngredients.value = recipe.value.recipeIngredient.map((ingredient) => {
|
||||||
|
return {
|
||||||
|
checked: true,
|
||||||
|
ingredient,
|
||||||
|
display: parseIngredientText(ingredient, recipe.value?.settings?.disableAmount || false, props.recipeScale),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
state.shoppingListDialog = false;
|
||||||
|
state.shoppingListIngredientDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bulkCheckIngredients(value = true) {
|
||||||
|
recipeIngredients.value.forEach((data) => {
|
||||||
|
data.checked = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addRecipeToList() {
|
||||||
|
if (!selectedShoppingList.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ingredients: RecipeIngredient[] = [];
|
||||||
|
recipeIngredients.value.forEach((data) => {
|
||||||
|
if (data.checked) {
|
||||||
|
ingredients.push(data.ingredient);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ingredients.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await api.shopping.lists.addRecipe(
|
||||||
|
selectedShoppingList.value.id,
|
||||||
|
props.recipeId,
|
||||||
|
props.recipeScale,
|
||||||
|
ingredients
|
||||||
|
);
|
||||||
if (data) {
|
if (data) {
|
||||||
alert.success(i18n.t("recipe.recipe-added-to-list") as string);
|
alert.success(i18n.t("recipe.recipe-added-to-list") as string);
|
||||||
state.shoppingListDialog = false;
|
state.shoppingListDialog = false;
|
||||||
|
state.shoppingListIngredientDialog = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,7 +515,9 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
shoppingList: () => {
|
shoppingList: () => {
|
||||||
getShoppingLists();
|
getShoppingLists();
|
||||||
|
|
||||||
state.shoppingListDialog = true;
|
state.shoppingListDialog = true;
|
||||||
|
state.shoppingListIngredientDialog = false;
|
||||||
},
|
},
|
||||||
share: () => {
|
share: () => {
|
||||||
state.shareDialog = true;
|
state.shareDialog = true;
|
||||||
@ -435,14 +548,27 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
shoppingLists,
|
shoppingLists,
|
||||||
|
selectedShoppingList,
|
||||||
|
openShoppingListIngredientDialog,
|
||||||
addRecipeToList,
|
addRecipeToList,
|
||||||
|
bulkCheckIngredients,
|
||||||
duplicateRecipe,
|
duplicateRecipe,
|
||||||
contextMenuEventHandler,
|
contextMenuEventHandler,
|
||||||
deleteRecipe,
|
deleteRecipe,
|
||||||
addRecipeToPlan,
|
addRecipeToPlan,
|
||||||
icon,
|
icon,
|
||||||
planTypeOptions,
|
planTypeOptions,
|
||||||
|
recipeIngredients,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="css">
|
||||||
|
.ingredient-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-gap: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -604,6 +604,7 @@
|
|||||||
"delete-checked": "Delete Checked",
|
"delete-checked": "Delete Checked",
|
||||||
"toggle-label-sort": "Toggle Label Sort",
|
"toggle-label-sort": "Toggle Label Sort",
|
||||||
"uncheck-all-items": "Uncheck All Items",
|
"uncheck-all-items": "Uncheck All Items",
|
||||||
|
"check-all-items": "Check All Items",
|
||||||
"linked-recipes-count": "No Linked Recipes|One Linked Recipe|{count} Linked Recipes",
|
"linked-recipes-count": "No Linked Recipes|One Linked Recipe|{count} Linked Recipes",
|
||||||
"items-checked-count": "No items checked|One item checked|{count} items checked",
|
"items-checked-count": "No items checked|One item checked|{count} items checked",
|
||||||
"no-label": "No Label"
|
"no-label": "No Label"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { BaseCRUDAPI } from "../base/base-clients";
|
import { BaseCRUDAPI } from "../base/base-clients";
|
||||||
|
import { RecipeIngredient } from "../types/recipe";
|
||||||
import { ApiRequestInstance } from "~/lib/api/types/non-generated";
|
import { ApiRequestInstance } from "~/lib/api/types/non-generated";
|
||||||
import {
|
import {
|
||||||
ShoppingListCreate,
|
ShoppingListCreate,
|
||||||
@ -25,12 +26,12 @@ export class ShoppingListsApi extends BaseCRUDAPI<ShoppingListCreate, ShoppingLi
|
|||||||
baseRoute = routes.shoppingLists;
|
baseRoute = routes.shoppingLists;
|
||||||
itemRoute = routes.shoppingListsId;
|
itemRoute = routes.shoppingListsId;
|
||||||
|
|
||||||
async addRecipe(itemId: string, recipeId: string, recipeIncrementQuantity = 1) {
|
async addRecipe(itemId: string, recipeId: string, recipeIncrementQuantity = 1, recipeIngredients: RecipeIngredient[] | null = null) {
|
||||||
return await this.requests.post(routes.shoppingListIdAddRecipe(itemId, recipeId), {recipeIncrementQuantity});
|
return await this.requests.post(routes.shoppingListIdAddRecipe(itemId, recipeId), { recipeIncrementQuantity, recipeIngredients });
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeRecipe(itemId: string, recipeId: string, recipeDecrementQuantity = 1) {
|
async removeRecipe(itemId: string, recipeId: string, recipeDecrementQuantity = 1) {
|
||||||
return await this.requests.post(routes.shoppingListIdRemoveRecipe(itemId, recipeId), {recipeDecrementQuantity});
|
return await this.requests.post(routes.shoppingListIdRemoveRecipe(itemId, recipeId), { recipeDecrementQuantity });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,6 +133,7 @@ import {
|
|||||||
mdiDockRight,
|
mdiDockRight,
|
||||||
mdiDockTop,
|
mdiDockTop,
|
||||||
mdiDockBottom,
|
mdiDockBottom,
|
||||||
|
mdiCheckboxOutline,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
|
|
||||||
export const icons = {
|
export const icons = {
|
||||||
@ -167,6 +168,7 @@ export const icons = {
|
|||||||
cartCheck: mdiCartCheck,
|
cartCheck: mdiCartCheck,
|
||||||
check: mdiCheck,
|
check: mdiCheck,
|
||||||
checkboxBlankOutline: mdiCheckboxBlankOutline,
|
checkboxBlankOutline: mdiCheckboxBlankOutline,
|
||||||
|
checkboxOutline: mdiCheckboxOutline,
|
||||||
checkboxMarkedCircle: mdiCheckboxMarkedCircle,
|
checkboxMarkedCircle: mdiCheckboxMarkedCircle,
|
||||||
chefHat: mdiChefHat,
|
chefHat: mdiChefHat,
|
||||||
clipboardCheck: mdiClipboardCheck,
|
clipboardCheck: mdiClipboardCheck,
|
||||||
|
@ -228,7 +228,7 @@ class ShoppingListController(BaseCrudController):
|
|||||||
self, item_id: UUID4, recipe_id: UUID4, data: ShoppingListAddRecipeParams | None = None
|
self, item_id: UUID4, recipe_id: UUID4, data: ShoppingListAddRecipeParams | None = None
|
||||||
):
|
):
|
||||||
shopping_list, items = self.service.add_recipe_ingredients_to_list(
|
shopping_list, items = self.service.add_recipe_ingredients_to_list(
|
||||||
item_id, recipe_id, data.recipe_increment_quantity if data else 1
|
item_id, recipe_id, data.recipe_increment_quantity if data else 1, data.recipe_ingredients if data else None
|
||||||
)
|
)
|
||||||
|
|
||||||
publish_list_item_events(self.publish_event, items)
|
publish_list_item_events(self.publish_event, items)
|
||||||
|
@ -14,6 +14,7 @@ from mealie.schema.recipe.recipe_ingredient import (
|
|||||||
MAX_INGREDIENT_DENOMINATOR,
|
MAX_INGREDIENT_DENOMINATOR,
|
||||||
IngredientFood,
|
IngredientFood,
|
||||||
IngredientUnit,
|
IngredientUnit,
|
||||||
|
RecipeIngredient,
|
||||||
)
|
)
|
||||||
from mealie.schema.response.pagination import PaginationBase
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
|
|
||||||
@ -237,6 +238,8 @@ class ShoppingListOut(ShoppingListUpdate):
|
|||||||
|
|
||||||
class ShoppingListAddRecipeParams(MealieModel):
|
class ShoppingListAddRecipeParams(MealieModel):
|
||||||
recipe_increment_quantity: float = 1
|
recipe_increment_quantity: float = 1
|
||||||
|
recipe_ingredients: list[RecipeIngredient] | None = None
|
||||||
|
"""optionally override which ingredients are added from the recipe"""
|
||||||
|
|
||||||
|
|
||||||
class ShoppingListRemoveRecipeParams(MealieModel):
|
class ShoppingListRemoveRecipeParams(MealieModel):
|
||||||
|
@ -14,7 +14,11 @@ from mealie.schema.group.group_shopping_list import (
|
|||||||
ShoppingListItemUpdate,
|
ShoppingListItemUpdate,
|
||||||
ShoppingListItemUpdateBulk,
|
ShoppingListItemUpdateBulk,
|
||||||
)
|
)
|
||||||
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
|
from mealie.schema.recipe.recipe_ingredient import (
|
||||||
|
IngredientFood,
|
||||||
|
IngredientUnit,
|
||||||
|
RecipeIngredient,
|
||||||
|
)
|
||||||
from mealie.schema.response.pagination import PaginationQuery
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
|
|
||||||
|
|
||||||
@ -265,16 +269,23 @@ class ShoppingListService:
|
|||||||
return ShoppingListItemsCollectionOut(created_items=[], updated_items=[], deleted_items=deleted_items)
|
return ShoppingListItemsCollectionOut(created_items=[], updated_items=[], deleted_items=deleted_items)
|
||||||
|
|
||||||
def get_shopping_list_items_from_recipe(
|
def get_shopping_list_items_from_recipe(
|
||||||
self, list_id: UUID4, recipe_id: UUID4, scale: float = 1
|
self,
|
||||||
|
list_id: UUID4,
|
||||||
|
recipe_id: UUID4,
|
||||||
|
scale: float = 1,
|
||||||
|
recipe_ingredients: list[RecipeIngredient] | None = None,
|
||||||
) -> list[ShoppingListItemCreate]:
|
) -> list[ShoppingListItemCreate]:
|
||||||
"""Generates a list of new list items based on a recipe"""
|
"""Generates a list of new list items based on a recipe"""
|
||||||
|
|
||||||
recipe = self.repos.recipes.get_one(recipe_id, "id")
|
if recipe_ingredients is None:
|
||||||
if not recipe:
|
recipe = self.repos.recipes.get_one(recipe_id, "id")
|
||||||
raise UnexpectedNone("Recipe not found")
|
if not recipe:
|
||||||
|
raise UnexpectedNone("Recipe not found")
|
||||||
|
|
||||||
|
recipe_ingredients = recipe.recipe_ingredient
|
||||||
|
|
||||||
list_items: list[ShoppingListItemCreate] = []
|
list_items: list[ShoppingListItemCreate] = []
|
||||||
for ingredient in recipe.recipe_ingredient:
|
for ingredient in recipe_ingredients:
|
||||||
if isinstance(ingredient.food, IngredientFood):
|
if isinstance(ingredient.food, IngredientFood):
|
||||||
is_food = True
|
is_food = True
|
||||||
food_id = ingredient.food.id
|
food_id = ingredient.food.id
|
||||||
@ -301,7 +312,7 @@ class ShoppingListService:
|
|||||||
unit_id=unit_id,
|
unit_id=unit_id,
|
||||||
recipe_references=[
|
recipe_references=[
|
||||||
ShoppingListItemRecipeRefCreate(
|
ShoppingListItemRecipeRefCreate(
|
||||||
recipe_id=recipe.id, recipe_quantity=ingredient.quantity, recipe_scale=scale
|
recipe_id=recipe_id, recipe_quantity=ingredient.quantity, recipe_scale=scale
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -331,7 +342,11 @@ class ShoppingListService:
|
|||||||
return list_items
|
return list_items
|
||||||
|
|
||||||
def add_recipe_ingredients_to_list(
|
def add_recipe_ingredients_to_list(
|
||||||
self, list_id: UUID4, recipe_id: UUID4, recipe_increment: float = 1
|
self,
|
||||||
|
list_id: UUID4,
|
||||||
|
recipe_id: UUID4,
|
||||||
|
recipe_increment: float = 1,
|
||||||
|
recipe_ingredients: list[RecipeIngredient] | None = None,
|
||||||
) -> tuple[ShoppingListOut, ShoppingListItemsCollectionOut]:
|
) -> tuple[ShoppingListOut, ShoppingListItemsCollectionOut]:
|
||||||
"""
|
"""
|
||||||
Adds a recipe's ingredients to a list
|
Adds a recipe's ingredients to a list
|
||||||
@ -341,7 +356,9 @@ class ShoppingListService:
|
|||||||
- Impacted Shopping List Items
|
- Impacted Shopping List Items
|
||||||
"""
|
"""
|
||||||
|
|
||||||
items_to_create = self.get_shopping_list_items_from_recipe(list_id, recipe_id, recipe_increment)
|
items_to_create = self.get_shopping_list_items_from_recipe(
|
||||||
|
list_id, recipe_id, recipe_increment, recipe_ingredients
|
||||||
|
)
|
||||||
item_changes = self.bulk_create_items(items_to_create)
|
item_changes = self.bulk_create_items(items_to_create)
|
||||||
|
|
||||||
updated_list = cast(ShoppingListOut, self.shopping_lists.get_one(list_id))
|
updated_list = cast(ShoppingListOut, self.shopping_lists.get_one(list_id))
|
||||||
|
@ -4,6 +4,7 @@ from fastapi.testclient import TestClient
|
|||||||
|
|
||||||
from mealie.schema.group.group_shopping_list import ShoppingListOut
|
from mealie.schema.group.group_shopping_list import ShoppingListOut
|
||||||
from mealie.schema.recipe.recipe import Recipe
|
from mealie.schema.recipe.recipe import Recipe
|
||||||
|
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient
|
||||||
from tests import utils
|
from tests import utils
|
||||||
from tests.utils import api_routes
|
from tests.utils import api_routes
|
||||||
from tests.utils.factories import random_int, random_string
|
from tests.utils.factories import random_int, random_string
|
||||||
@ -186,6 +187,52 @@ def test_shopping_lists_add_one_with_zero_quantity(
|
|||||||
assert found
|
assert found
|
||||||
|
|
||||||
|
|
||||||
|
def test_shopping_lists_add_custom_recipe_items(
|
||||||
|
api_client: TestClient,
|
||||||
|
unique_user: TestUser,
|
||||||
|
shopping_lists: list[ShoppingListOut],
|
||||||
|
recipe_ingredient_only: Recipe,
|
||||||
|
):
|
||||||
|
sample_list = random.choice(shopping_lists)
|
||||||
|
recipe = recipe_ingredient_only
|
||||||
|
|
||||||
|
response = api_client.post(
|
||||||
|
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id), headers=unique_user.token
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
custom_items = random.sample(recipe_ingredient_only.recipe_ingredient, k=3)
|
||||||
|
response = api_client.post(
|
||||||
|
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
||||||
|
headers=unique_user.token,
|
||||||
|
json={"recipeIngredients": utils.jsonify(custom_items)},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# get list and verify items against ingredients
|
||||||
|
response = api_client.get(api_routes.groups_shopping_lists_item_id(sample_list.id), headers=unique_user.token)
|
||||||
|
as_json = utils.assert_derserialize(response, 200)
|
||||||
|
assert len(as_json["listItems"]) == len(recipe.recipe_ingredient)
|
||||||
|
|
||||||
|
known_ingredients = {ingredient.note: ingredient for ingredient in recipe.recipe_ingredient}
|
||||||
|
custom_ingredients = [ingredient.note for ingredient in custom_items]
|
||||||
|
for item in as_json["listItems"]:
|
||||||
|
assert item["note"] in known_ingredients
|
||||||
|
|
||||||
|
ingredient = known_ingredients[item["note"]]
|
||||||
|
if item["note"] in custom_ingredients:
|
||||||
|
assert item["quantity"] == (ingredient.quantity * 2 if ingredient.quantity else 0)
|
||||||
|
|
||||||
|
else:
|
||||||
|
assert item["quantity"] == (ingredient.quantity or 0)
|
||||||
|
|
||||||
|
# check recipe reference was added with quantity 2
|
||||||
|
refs = as_json["recipeReferences"]
|
||||||
|
assert len(refs) == 1
|
||||||
|
assert refs[0]["recipeId"] == str(recipe.id)
|
||||||
|
assert refs[0]["recipeQuantity"] == 2
|
||||||
|
|
||||||
|
|
||||||
def test_shopping_list_ref_removes_itself(
|
def test_shopping_list_ref_removes_itself(
|
||||||
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut, recipe_ingredient_only: Recipe
|
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut, recipe_ingredient_only: Recipe
|
||||||
):
|
):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user