mirror of
https://github.com/hay-kot/mealie.git
synced 2025-07-06 04:52:25 -07:00
fix: Bulk Add Recipes to Shopping List (#5054)
This commit is contained in:
parent
3d1b76bcad
commit
716c85cc3b
13 changed files with 306 additions and 77 deletions
|
@ -138,8 +138,8 @@ import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
import { useShoppingListPreferences } from "~/composables/use-users/preferences";
|
import { useShoppingListPreferences } from "~/composables/use-users/preferences";
|
||||||
import { ShoppingListSummary } from "~/lib/api/types/household";
|
import { RecipeIngredient, ShoppingListAddRecipeParamsBulk, ShoppingListSummary } from "~/lib/api/types/household";
|
||||||
import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
|
import { Recipe } from "~/lib/api/types/recipe";
|
||||||
|
|
||||||
export interface RecipeWithScale extends Recipe {
|
export interface RecipeWithScale extends Recipe {
|
||||||
scale: number;
|
scale: number;
|
||||||
|
@ -342,12 +342,12 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addRecipesToList() {
|
async function addRecipesToList() {
|
||||||
const promises: Promise<any>[] = [];
|
|
||||||
recipeIngredientSections.value.forEach((section) => {
|
|
||||||
if (!selectedShoppingList.value) {
|
if (!selectedShoppingList.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipeData: ShoppingListAddRecipeParamsBulk[] = [];
|
||||||
|
recipeIngredientSections.value.forEach((section) => {
|
||||||
const ingredients: RecipeIngredient[] = [];
|
const ingredients: RecipeIngredient[] = [];
|
||||||
section.ingredientSections.forEach((ingSection) => {
|
section.ingredientSections.forEach((ingSection) => {
|
||||||
ingSection.ingredients.forEach((ing) => {
|
ingSection.ingredients.forEach((ing) => {
|
||||||
|
@ -361,24 +361,18 @@ export default defineComponent({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
promises.push(api.shopping.lists.addRecipe(
|
recipeData.push(
|
||||||
selectedShoppingList.value.id,
|
{
|
||||||
section.recipeId,
|
recipeId: section.recipeId,
|
||||||
section.recipeScale,
|
recipeIncrementQuantity: section.recipeScale,
|
||||||
ingredients,
|
recipeIngredients: ingredients,
|
||||||
));
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let success = true;
|
const { error } = await api.shopping.lists.addRecipes(selectedShoppingList.value.id, recipeData);
|
||||||
const results = await Promise.allSettled(promises);
|
error ? alert.error(i18n.tc("recipe.failed-to-add-recipes-to-list"))
|
||||||
results.forEach((result) => {
|
: alert.success(i18n.tc("recipe.successfully-added-to-list"));
|
||||||
if (result.status === "rejected") {
|
|
||||||
success = false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
success ? alert.success(i18n.tc("recipe.successfully-added-to-list"))
|
|
||||||
: alert.error(i18n.tc("failed-to-add-recipes-to-list"))
|
|
||||||
|
|
||||||
state.shoppingListDialog = false;
|
state.shoppingListDialog = false;
|
||||||
state.shoppingListIngredientDialog = false;
|
state.shoppingListIngredientDialog = false;
|
||||||
|
|
|
@ -391,6 +391,11 @@ export interface CreateIngredientFoodAlias {
|
||||||
name: string;
|
name: string;
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
}
|
}
|
||||||
|
export interface ShoppingListAddRecipeParamsBulk {
|
||||||
|
recipeIncrementQuantity?: number;
|
||||||
|
recipeIngredients?: RecipeIngredient[] | null;
|
||||||
|
recipeId: string;
|
||||||
|
}
|
||||||
export interface ShoppingListCreate {
|
export interface ShoppingListCreate {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
extras?: {
|
extras?: {
|
||||||
|
|
|
@ -291,6 +291,7 @@ export interface UserBase {
|
||||||
id: string;
|
id: string;
|
||||||
username?: string | null;
|
username?: string | null;
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
|
fullName?: string | null;
|
||||||
}
|
}
|
||||||
export interface RecipeCategoryResponse {
|
export interface RecipeCategoryResponse {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
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 {
|
||||||
|
ShoppingListAddRecipeParamsBulk,
|
||||||
ShoppingListCreate,
|
ShoppingListCreate,
|
||||||
ShoppingListItemCreate,
|
ShoppingListItemCreate,
|
||||||
ShoppingListItemOut,
|
ShoppingListItemOut,
|
||||||
|
@ -16,7 +16,7 @@ const prefix = "/api";
|
||||||
const routes = {
|
const routes = {
|
||||||
shoppingLists: `${prefix}/households/shopping/lists`,
|
shoppingLists: `${prefix}/households/shopping/lists`,
|
||||||
shoppingListsId: (id: string) => `${prefix}/households/shopping/lists/${id}`,
|
shoppingListsId: (id: string) => `${prefix}/households/shopping/lists/${id}`,
|
||||||
shoppingListIdAddRecipe: (id: string, recipeId: string) => `${prefix}/households/shopping/lists/${id}/recipe/${recipeId}`,
|
shoppingListIdAddRecipe: (id: string) => `${prefix}/households/shopping/lists/${id}/recipe`,
|
||||||
shoppingListIdRemoveRecipe: (id: string, recipeId: string) => `${prefix}/households/shopping/lists/${id}/recipe/${recipeId}/delete`,
|
shoppingListIdRemoveRecipe: (id: string, recipeId: string) => `${prefix}/households/shopping/lists/${id}/recipe/${recipeId}/delete`,
|
||||||
shoppingListIdUpdateLabelSettings: (id: string) => `${prefix}/households/shopping/lists/${id}/label-settings`,
|
shoppingListIdUpdateLabelSettings: (id: string) => `${prefix}/households/shopping/lists/${id}/label-settings`,
|
||||||
|
|
||||||
|
@ -29,8 +29,8 @@ 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, recipeIngredients: RecipeIngredient[] | null = null) {
|
async addRecipes(itemId: string, data: ShoppingListAddRecipeParamsBulk[]) {
|
||||||
return await this.requests.post(routes.shoppingListIdAddRecipe(itemId, recipeId), { recipeIncrementQuantity, recipeIngredients });
|
return await this.requests.post(routes.shoppingListIdAddRecipe(itemId), data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeRecipe(itemId: string, recipeId: string, recipeDecrementQuantity = 1) {
|
async removeRecipe(itemId: string, recipeId: string, recipeDecrementQuantity = 1) {
|
||||||
|
|
|
@ -834,7 +834,7 @@ export default defineComponent({
|
||||||
|
|
||||||
loadingCounter.value += 1;
|
loadingCounter.value += 1;
|
||||||
recipeReferenceLoading.value = true;
|
recipeReferenceLoading.value = true;
|
||||||
const { data } = await userApi.shopping.lists.addRecipe(shoppingList.value.id, recipeId);
|
const { data } = await userApi.shopping.lists.addRecipes(shoppingList.value.id, [{ recipeId }]);
|
||||||
recipeReferenceLoading.value = false;
|
recipeReferenceLoading.value = false;
|
||||||
loadingCounter.value -= 1;
|
loadingCounter.value -= 1;
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ from mealie.routes._base.controller import controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
from mealie.schema.household.group_shopping_list import (
|
from mealie.schema.household.group_shopping_list import (
|
||||||
ShoppingListAddRecipeParams,
|
ShoppingListAddRecipeParams,
|
||||||
|
ShoppingListAddRecipeParamsBulk,
|
||||||
ShoppingListCreate,
|
ShoppingListCreate,
|
||||||
ShoppingListItemCreate,
|
ShoppingListItemCreate,
|
||||||
ShoppingListItemOut,
|
ShoppingListItemOut,
|
||||||
|
@ -252,17 +253,24 @@ class ShoppingListController(BaseCrudController):
|
||||||
|
|
||||||
return updated_list
|
return updated_list
|
||||||
|
|
||||||
@router.post("/{item_id}/recipe/{recipe_id}", response_model=ShoppingListOut)
|
@router.post("/{item_id}/recipe", response_model=ShoppingListOut)
|
||||||
def add_recipe_ingredients_to_list(
|
def add_recipe_ingredients_to_list(self, item_id: UUID4, data: list[ShoppingListAddRecipeParamsBulk]):
|
||||||
self, item_id: UUID4, recipe_id: UUID4, data: ShoppingListAddRecipeParams | None = None
|
shopping_list, items = self.service.add_recipe_ingredients_to_list(item_id, data)
|
||||||
):
|
|
||||||
shopping_list, items = self.service.add_recipe_ingredients_to_list(
|
|
||||||
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)
|
||||||
return shopping_list
|
return shopping_list
|
||||||
|
|
||||||
|
@router.post("/{item_id}/recipe/{recipe_id}", response_model=ShoppingListOut, deprecated=True)
|
||||||
|
def add_single_recipe_ingredients_to_list(
|
||||||
|
self, item_id: UUID4, recipe_id: UUID4, data: ShoppingListAddRecipeParams | None = None
|
||||||
|
):
|
||||||
|
# Compatibility function for old API
|
||||||
|
# TODO: remove this function in the future
|
||||||
|
|
||||||
|
data = data or ShoppingListAddRecipeParams(recipe_increment_quantity=1)
|
||||||
|
bulk_data = [data.cast(ShoppingListAddRecipeParamsBulk, recipe_id=recipe_id)]
|
||||||
|
return self.add_recipe_ingredients_to_list(item_id, bulk_data)
|
||||||
|
|
||||||
@router.post("/{item_id}/recipe/{recipe_id}/delete", response_model=ShoppingListOut)
|
@router.post("/{item_id}/recipe/{recipe_id}/delete", response_model=ShoppingListOut)
|
||||||
def remove_recipe_ingredients_from_list(
|
def remove_recipe_ingredients_from_list(
|
||||||
self, item_id: UUID4, recipe_id: UUID4, data: ShoppingListRemoveRecipeParams | None = None
|
self, item_id: UUID4, recipe_id: UUID4, data: ShoppingListRemoveRecipeParams | None = None
|
||||||
|
|
|
@ -20,6 +20,7 @@ from .group_recipe_action import (
|
||||||
)
|
)
|
||||||
from .group_shopping_list import (
|
from .group_shopping_list import (
|
||||||
ShoppingListAddRecipeParams,
|
ShoppingListAddRecipeParams,
|
||||||
|
ShoppingListAddRecipeParamsBulk,
|
||||||
ShoppingListCreate,
|
ShoppingListCreate,
|
||||||
ShoppingListItemBase,
|
ShoppingListItemBase,
|
||||||
ShoppingListItemCreate,
|
ShoppingListItemCreate,
|
||||||
|
@ -113,6 +114,7 @@ __all__ = [
|
||||||
"ReadInviteToken",
|
"ReadInviteToken",
|
||||||
"SaveInviteToken",
|
"SaveInviteToken",
|
||||||
"ShoppingListAddRecipeParams",
|
"ShoppingListAddRecipeParams",
|
||||||
|
"ShoppingListAddRecipeParamsBulk",
|
||||||
"ShoppingListCreate",
|
"ShoppingListCreate",
|
||||||
"ShoppingListItemBase",
|
"ShoppingListItemBase",
|
||||||
"ShoppingListItemCreate",
|
"ShoppingListItemCreate",
|
||||||
|
|
|
@ -292,5 +292,9 @@ class ShoppingListAddRecipeParams(MealieModel):
|
||||||
"""optionally override which ingredients are added from the recipe"""
|
"""optionally override which ingredients are added from the recipe"""
|
||||||
|
|
||||||
|
|
||||||
|
class ShoppingListAddRecipeParamsBulk(ShoppingListAddRecipeParams):
|
||||||
|
recipe_id: UUID4
|
||||||
|
|
||||||
|
|
||||||
class ShoppingListRemoveRecipeParams(MealieModel):
|
class ShoppingListRemoveRecipeParams(MealieModel):
|
||||||
recipe_decrement_quantity: float = 1
|
recipe_decrement_quantity: float = 1
|
||||||
|
|
|
@ -5,6 +5,7 @@ from pydantic import UUID4
|
||||||
from mealie.core.exceptions import UnexpectedNone
|
from mealie.core.exceptions import UnexpectedNone
|
||||||
from mealie.repos.repository_factory import AllRepositories
|
from mealie.repos.repository_factory import AllRepositories
|
||||||
from mealie.schema.household.group_shopping_list import (
|
from mealie.schema.household.group_shopping_list import (
|
||||||
|
ShoppingListAddRecipeParamsBulk,
|
||||||
ShoppingListCreate,
|
ShoppingListCreate,
|
||||||
ShoppingListItemBase,
|
ShoppingListItemBase,
|
||||||
ShoppingListItemCreate,
|
ShoppingListItemCreate,
|
||||||
|
@ -135,11 +136,11 @@ class ShoppingListService:
|
||||||
consolidated_create_items: list[ShoppingListItemCreate] = []
|
consolidated_create_items: list[ShoppingListItemCreate] = []
|
||||||
for create_item in create_items:
|
for create_item in create_items:
|
||||||
merged = False
|
merged = False
|
||||||
for filtered_item in consolidated_create_items:
|
for i, filtered_item in enumerate(consolidated_create_items):
|
||||||
if not self.can_merge(create_item, filtered_item):
|
if not self.can_merge(create_item, filtered_item):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
filtered_item = self.merge_items(create_item, filtered_item).cast(ShoppingListItemCreate)
|
consolidated_create_items[i] = self.merge_items(create_item, filtered_item).cast(ShoppingListItemCreate)
|
||||||
merged = True
|
merged = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -207,11 +208,11 @@ class ShoppingListService:
|
||||||
seen_update_ids.add(update_item.id)
|
seen_update_ids.add(update_item.id)
|
||||||
|
|
||||||
merged = False
|
merged = False
|
||||||
for filtered_item in consolidated_update_items:
|
for i, filtered_item in enumerate(consolidated_update_items):
|
||||||
if not self.can_merge(update_item, filtered_item):
|
if not self.can_merge(update_item, filtered_item):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
filtered_item = self.merge_items(update_item, filtered_item).cast(
|
consolidated_update_items[i] = self.merge_items(update_item, filtered_item).cast(
|
||||||
ShoppingListItemUpdateBulk, id=filtered_item.id
|
ShoppingListItemUpdateBulk, id=filtered_item.id
|
||||||
)
|
)
|
||||||
delete_items.add(update_item.id)
|
delete_items.add(update_item.id)
|
||||||
|
@ -373,37 +374,42 @@ class ShoppingListService:
|
||||||
def add_recipe_ingredients_to_list(
|
def add_recipe_ingredients_to_list(
|
||||||
self,
|
self,
|
||||||
list_id: UUID4,
|
list_id: UUID4,
|
||||||
recipe_id: UUID4,
|
recipe_items: list[ShoppingListAddRecipeParamsBulk],
|
||||||
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 recipe ingredients to a list
|
||||||
|
|
||||||
Returns a tuple of:
|
Returns a tuple of:
|
||||||
- Updated Shopping List
|
- Updated Shopping List
|
||||||
- Impacted Shopping List Items
|
- Impacted Shopping List Items
|
||||||
"""
|
"""
|
||||||
|
|
||||||
items_to_create = self.get_shopping_list_items_from_recipe(
|
items_to_create = [
|
||||||
list_id, recipe_id, recipe_increment, recipe_ingredients
|
item
|
||||||
|
for recipe in recipe_items
|
||||||
|
for item in self.get_shopping_list_items_from_recipe(
|
||||||
|
list_id, recipe.recipe_id, recipe.recipe_increment_quantity, recipe.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))
|
||||||
|
|
||||||
|
# update list-level recipe references
|
||||||
|
for recipe in recipe_items:
|
||||||
ref_merged = False
|
ref_merged = False
|
||||||
for ref in updated_list.recipe_references:
|
for ref in updated_list.recipe_references:
|
||||||
if ref.recipe_id != recipe_id:
|
if ref.recipe_id != recipe.recipe_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ref.recipe_quantity += recipe_increment
|
ref.recipe_quantity += recipe.recipe_increment_quantity
|
||||||
ref_merged = True
|
ref_merged = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if not ref_merged:
|
if not ref_merged:
|
||||||
updated_list.recipe_references.append(
|
updated_list.recipe_references.append(
|
||||||
ShoppingListItemRecipeRefCreate(recipe_id=recipe_id, recipe_quantity=recipe_increment) # type: ignore
|
ShoppingListItemRecipeRefCreate(
|
||||||
|
recipe_id=recipe.recipe_id, recipe_quantity=recipe.recipe_increment_quantity
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
updated_list = self.shopping_lists.update(updated_list.id, updated_list)
|
updated_list = self.shopping_lists.update(updated_list.id, updated_list)
|
||||||
|
|
31
tests/fixtures/fixture_recipe.py
vendored
31
tests/fixtures/fixture_recipe.py
vendored
|
@ -54,6 +54,37 @@ def recipe_ingredient_only(unique_user: TestUser):
|
||||||
database.recipes.delete(model.slug)
|
database.recipes.delete(model.slug)
|
||||||
|
|
||||||
|
|
||||||
|
@fixture(scope="function")
|
||||||
|
def recipes_ingredient_only(unique_user: TestUser):
|
||||||
|
database = unique_user.repos
|
||||||
|
recipes: list[Recipe] = []
|
||||||
|
|
||||||
|
for _ in range(3):
|
||||||
|
# Create a recipe
|
||||||
|
recipe = Recipe(
|
||||||
|
user_id=unique_user.user_id,
|
||||||
|
group_id=unique_user.group_id,
|
||||||
|
name=random_string(10),
|
||||||
|
recipe_ingredient=[
|
||||||
|
RecipeIngredient(note=f"Ingredient 1 {random_string(5)}"),
|
||||||
|
RecipeIngredient(note=f"Ingredient 2 {random_string(5)}"),
|
||||||
|
RecipeIngredient(note=f"Ingredient 3 {random_string(5)}"),
|
||||||
|
RecipeIngredient(note=f"Ingredient 4 {random_string(5)}"),
|
||||||
|
RecipeIngredient(note=f"Ingredient 5 {random_string(5)}"),
|
||||||
|
RecipeIngredient(note=f"Ingredient 6 {random_string(5)}"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
model = database.recipes.create(recipe)
|
||||||
|
recipes.append(model)
|
||||||
|
|
||||||
|
yield recipes
|
||||||
|
|
||||||
|
with contextlib.suppress(sqlalchemy.exc.NoResultFound):
|
||||||
|
for recipe in recipes:
|
||||||
|
database.recipes.delete(recipe.slug)
|
||||||
|
|
||||||
|
|
||||||
@fixture(scope="function")
|
@fixture(scope="function")
|
||||||
def recipe_categories(unique_user: TestUser) -> Generator[list[CategoryOut], None, None]:
|
def recipe_categories(unique_user: TestUser) -> Generator[list[CategoryOut], None, None]:
|
||||||
database = unique_user.repos
|
database = unique_user.repos
|
||||||
|
|
|
@ -6,7 +6,6 @@ import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.repos.repository_factory import AllRepositories
|
|
||||||
from mealie.schema.household.group_shopping_list import ShoppingListItemOut, ShoppingListOut
|
from mealie.schema.household.group_shopping_list import ShoppingListItemOut, ShoppingListOut
|
||||||
from mealie.schema.recipe.recipe_ingredient import SaveIngredientFood
|
from mealie.schema.recipe.recipe_ingredient import SaveIngredientFood
|
||||||
from tests import utils
|
from tests import utils
|
||||||
|
|
|
@ -3,6 +3,7 @@ import random
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from mealie.schema.household.group_shopping_list import (
|
from mealie.schema.household.group_shopping_list import (
|
||||||
|
ShoppingListAddRecipeParamsBulk,
|
||||||
ShoppingListItemOut,
|
ShoppingListItemOut,
|
||||||
ShoppingListItemUpdate,
|
ShoppingListItemUpdate,
|
||||||
ShoppingListItemUpdateBulk,
|
ShoppingListItemUpdateBulk,
|
||||||
|
@ -171,6 +172,78 @@ def test_shopping_lists_add_recipe(
|
||||||
assert refs[0]["recipeQuantity"] == 2
|
assert refs[0]["recipeQuantity"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_shopping_lists_add_recipes(
|
||||||
|
api_client: TestClient,
|
||||||
|
unique_user: TestUser,
|
||||||
|
shopping_lists: list[ShoppingListOut],
|
||||||
|
recipes_ingredient_only: list[Recipe],
|
||||||
|
):
|
||||||
|
sample_list = random.choice(shopping_lists)
|
||||||
|
recipes = recipes_ingredient_only
|
||||||
|
all_ingredients = [ingredient for recipe in recipes for ingredient in recipe.recipe_ingredient]
|
||||||
|
|
||||||
|
recipes_post_data = utils.jsonify(
|
||||||
|
[ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump() for recipe in recipes]
|
||||||
|
)
|
||||||
|
response = api_client.post(
|
||||||
|
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||||
|
json=recipes_post_data,
|
||||||
|
headers=unique_user.token,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# get list and verify items against ingredients
|
||||||
|
response = api_client.get(
|
||||||
|
api_routes.households_shopping_lists_item_id(sample_list.id),
|
||||||
|
headers=unique_user.token,
|
||||||
|
)
|
||||||
|
as_json = utils.assert_deserialize(response, 200)
|
||||||
|
assert len(as_json["listItems"]) == len(all_ingredients)
|
||||||
|
|
||||||
|
known_ingredients = {ingredient.note: ingredient for ingredient in all_ingredients}
|
||||||
|
for item in as_json["listItems"]:
|
||||||
|
assert item["note"] in known_ingredients
|
||||||
|
|
||||||
|
ingredient = known_ingredients[item["note"]]
|
||||||
|
assert item["quantity"] == (ingredient.quantity or 0)
|
||||||
|
|
||||||
|
# check recipe reference was added with quantity 1
|
||||||
|
refs = as_json["recipeReferences"]
|
||||||
|
assert len(refs) == len(recipes)
|
||||||
|
refs_by_id = {ref["recipeId"]: ref for ref in refs}
|
||||||
|
for recipe in recipes:
|
||||||
|
assert str(recipe.id) in refs_by_id
|
||||||
|
assert refs_by_id[str(recipe.id)]["recipeQuantity"] == 1
|
||||||
|
|
||||||
|
# add the recipes again and check the resulting items
|
||||||
|
response = api_client.post(
|
||||||
|
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||||
|
json=recipes_post_data,
|
||||||
|
headers=unique_user.token,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
response = api_client.get(
|
||||||
|
api_routes.households_shopping_lists_item_id(sample_list.id),
|
||||||
|
headers=unique_user.token,
|
||||||
|
)
|
||||||
|
as_json = utils.assert_deserialize(response, 200)
|
||||||
|
assert len(as_json["listItems"]) == len(all_ingredients)
|
||||||
|
|
||||||
|
for item in as_json["listItems"]:
|
||||||
|
assert item["note"] in known_ingredients
|
||||||
|
|
||||||
|
ingredient = known_ingredients[item["note"]]
|
||||||
|
assert item["quantity"] == (ingredient.quantity or 0) * 2
|
||||||
|
|
||||||
|
refs = as_json["recipeReferences"]
|
||||||
|
assert len(refs) == len(recipes)
|
||||||
|
refs_by_id = {ref["recipeId"]: ref for ref in refs}
|
||||||
|
for recipe in recipes:
|
||||||
|
assert str(recipe.id) in refs_by_id
|
||||||
|
assert refs_by_id[str(recipe.id)]["recipeQuantity"] == 2
|
||||||
|
|
||||||
|
|
||||||
def test_shopping_lists_add_one_with_zero_quantity(
|
def test_shopping_lists_add_one_with_zero_quantity(
|
||||||
api_client: TestClient,
|
api_client: TestClient,
|
||||||
unique_user: TestUser,
|
unique_user: TestUser,
|
||||||
|
@ -209,7 +282,8 @@ def test_shopping_lists_add_one_with_zero_quantity(
|
||||||
|
|
||||||
# add the recipe to the list and make sure there are three list items
|
# add the recipe to the list and make sure there are three list items
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
|
api_routes.households_shopping_lists_item_id_recipe(shopping_list.id),
|
||||||
|
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||||
headers=unique_user.token,
|
headers=unique_user.token,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -242,16 +316,19 @@ def test_shopping_lists_add_custom_recipe_items(
|
||||||
recipe = recipe_ingredient_only
|
recipe = recipe_ingredient_only
|
||||||
|
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||||
|
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||||
headers=unique_user.token,
|
headers=unique_user.token,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
custom_items = random.sample(recipe_ingredient_only.recipe_ingredient, k=3)
|
custom_items = random.sample(recipe_ingredient_only.recipe_ingredient, k=3)
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||||
|
json=utils.jsonify(
|
||||||
|
[ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id, recipe_ingredients=custom_items).model_dump()]
|
||||||
|
),
|
||||||
headers=unique_user.token,
|
headers=unique_user.token,
|
||||||
json={"recipeIngredients": utils.jsonify(custom_items)},
|
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
@ -291,7 +368,8 @@ def test_shopping_list_ref_removes_itself(
|
||||||
# add a recipe to a list, then check off all recipe items and make sure the recipe ref is deleted
|
# add a recipe to a list, then check off all recipe items and make sure the recipe ref is deleted
|
||||||
recipe = recipe_ingredient_only
|
recipe = recipe_ingredient_only
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
|
api_routes.households_shopping_lists_item_id_recipe(shopping_list.id),
|
||||||
|
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||||
headers=unique_user.token,
|
headers=unique_user.token,
|
||||||
)
|
)
|
||||||
utils.assert_deserialize(response, 200)
|
utils.assert_deserialize(response, 200)
|
||||||
|
@ -365,7 +443,8 @@ def test_shopping_lists_add_recipe_with_merge(
|
||||||
|
|
||||||
# add the recipe to the list and make sure there are only three list items, and their quantities/refs are correct
|
# add the recipe to the list and make sure there are only three list items, and their quantities/refs are correct
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
|
api_routes.households_shopping_lists_item_id_recipe(shopping_list.id),
|
||||||
|
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||||
headers=unique_user.token,
|
headers=unique_user.token,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -403,6 +482,85 @@ def test_shopping_lists_add_recipe_with_merge(
|
||||||
assert all([found_item_1, found_item_2, found_duplicate_item])
|
assert all([found_item_1, found_item_2, found_duplicate_item])
|
||||||
|
|
||||||
|
|
||||||
|
def test_shopping_lists_add_recipes_with_merge(
|
||||||
|
api_client: TestClient,
|
||||||
|
unique_user: TestUser,
|
||||||
|
shopping_lists: list[ShoppingListOut],
|
||||||
|
):
|
||||||
|
shopping_list = random.choice(shopping_lists)
|
||||||
|
|
||||||
|
# build two recipes that share an ingredient
|
||||||
|
recipes: list[Recipe] = []
|
||||||
|
common_note = random_string()
|
||||||
|
for _ in range(2):
|
||||||
|
response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=unique_user.token)
|
||||||
|
recipe_slug = utils.assert_deserialize(response, 201)
|
||||||
|
|
||||||
|
response = api_client.get(f"{api_routes.recipes}/{recipe_slug}", headers=unique_user.token)
|
||||||
|
recipe_data = utils.assert_deserialize(response, 200)
|
||||||
|
|
||||||
|
ingredient_unique = {"quantity": 1, "note": random_string()}
|
||||||
|
ingredient_duplicate = {"quantity": 1, "note": common_note}
|
||||||
|
|
||||||
|
recipe_data["recipeIngredient"] = [ingredient_unique, ingredient_duplicate]
|
||||||
|
response = api_client.put(
|
||||||
|
f"{api_routes.recipes}/{recipe_slug}",
|
||||||
|
json=recipe_data,
|
||||||
|
headers=unique_user.token,
|
||||||
|
)
|
||||||
|
utils.assert_deserialize(response, 200)
|
||||||
|
|
||||||
|
recipe = Recipe.model_validate_json(
|
||||||
|
api_client.get(f"{api_routes.recipes}/{recipe_slug}", headers=unique_user.token).content
|
||||||
|
)
|
||||||
|
assert recipe.id
|
||||||
|
assert len(recipe.recipe_ingredient) == 2
|
||||||
|
recipes.append(recipe)
|
||||||
|
|
||||||
|
# add the recipes to the list and make sure there are only five list items, and their quantities/refs are correct
|
||||||
|
response = api_client.post(
|
||||||
|
api_routes.households_shopping_lists_item_id_recipe(shopping_list.id),
|
||||||
|
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump() for recipe in recipes]),
|
||||||
|
headers=unique_user.token,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = api_client.get(
|
||||||
|
api_routes.households_shopping_lists_item_id(shopping_list.id),
|
||||||
|
headers=unique_user.token,
|
||||||
|
)
|
||||||
|
shopping_list_out = ShoppingListOut.model_validate(utils.assert_deserialize(response, 200))
|
||||||
|
|
||||||
|
assert len(shopping_list_out.list_items) == 3
|
||||||
|
|
||||||
|
found_recipe_1_item = False
|
||||||
|
found_recipe_2_item = False
|
||||||
|
found_duplicate_item = False
|
||||||
|
for list_item in shopping_list_out.list_items:
|
||||||
|
if list_item.note == common_note:
|
||||||
|
assert list_item.quantity == 2
|
||||||
|
assert len(list_item.recipe_references) == 2
|
||||||
|
assert {ref.recipe_id for ref in list_item.recipe_references} == {recipe.id for recipe in recipes}
|
||||||
|
found_duplicate_item = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
assert len(list_item.recipe_references) == 1
|
||||||
|
assert list_item.quantity == 1
|
||||||
|
if list_item.note == recipes[0].recipe_ingredient[0].note:
|
||||||
|
assert list_item.recipe_references[0].recipe_id == recipes[0].id
|
||||||
|
found_recipe_1_item = True
|
||||||
|
elif list_item.note == recipes[1].recipe_ingredient[0].note:
|
||||||
|
assert list_item.recipe_references[0].recipe_id == recipes[1].id
|
||||||
|
found_recipe_2_item = True
|
||||||
|
else:
|
||||||
|
raise Exception("Unexpected item")
|
||||||
|
|
||||||
|
for ref in list_item.recipe_references:
|
||||||
|
assert ref.recipe_scale == 1
|
||||||
|
assert ref.recipe_quantity == 1
|
||||||
|
|
||||||
|
assert all([found_recipe_1_item, found_recipe_2_item, found_duplicate_item])
|
||||||
|
|
||||||
|
|
||||||
def test_shopping_list_add_recipe_scale(
|
def test_shopping_list_add_recipe_scale(
|
||||||
api_client: TestClient,
|
api_client: TestClient,
|
||||||
unique_user: TestUser,
|
unique_user: TestUser,
|
||||||
|
@ -413,7 +571,8 @@ def test_shopping_list_add_recipe_scale(
|
||||||
recipe = recipe_ingredient_only
|
recipe = recipe_ingredient_only
|
||||||
|
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||||
|
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||||
headers=unique_user.token,
|
headers=unique_user.token,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -440,10 +599,12 @@ def test_shopping_list_add_recipe_scale(
|
||||||
assert refs[0]["recipeScale"] == 1
|
assert refs[0]["recipeScale"] == 1
|
||||||
|
|
||||||
recipe_scale = round(random.uniform(1, 10), 5)
|
recipe_scale = round(random.uniform(1, 10), 5)
|
||||||
payload = {"recipeIncrementQuantity": recipe_scale}
|
payload = utils.jsonify(
|
||||||
|
[ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id, recipe_increment_quantity=recipe_scale).model_dump()]
|
||||||
|
)
|
||||||
|
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||||
headers=unique_user.token,
|
headers=unique_user.token,
|
||||||
json=payload,
|
json=payload,
|
||||||
)
|
)
|
||||||
|
@ -476,9 +637,11 @@ def test_shopping_lists_remove_recipe(
|
||||||
recipe = recipe_ingredient_only
|
recipe = recipe_ingredient_only
|
||||||
|
|
||||||
# add two instances of the recipe
|
# add two instances of the recipe
|
||||||
payload = {"recipeIncrementQuantity": 2}
|
payload = utils.jsonify(
|
||||||
|
[ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id, recipe_increment_quantity=2).model_dump()]
|
||||||
|
)
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||||
json=payload,
|
json=payload,
|
||||||
headers=unique_user.token,
|
headers=unique_user.token,
|
||||||
)
|
)
|
||||||
|
@ -539,7 +702,8 @@ def test_shopping_lists_remove_recipe_multiple_quantity(
|
||||||
|
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||||
|
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||||
headers=unique_user.token,
|
headers=unique_user.token,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
@ -592,11 +756,17 @@ def test_shopping_list_remove_recipe_scale(
|
||||||
recipe = recipe_ingredient_only
|
recipe = recipe_ingredient_only
|
||||||
|
|
||||||
recipe_initital_scale = 100
|
recipe_initital_scale = 100
|
||||||
payload: dict = {"recipeIncrementQuantity": recipe_initital_scale}
|
payload = utils.jsonify(
|
||||||
|
[
|
||||||
|
ShoppingListAddRecipeParamsBulk(
|
||||||
|
recipe_id=recipe.id, recipe_increment_quantity=recipe_initital_scale
|
||||||
|
).model_dump()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# first add a bunch of quantity to the list
|
# first add a bunch of quantity to the list
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||||
headers=unique_user.token,
|
headers=unique_user.token,
|
||||||
json=payload,
|
json=payload,
|
||||||
)
|
)
|
||||||
|
@ -657,11 +827,13 @@ def test_recipe_decrement_max(
|
||||||
recipe = recipe_ingredient_only
|
recipe = recipe_ingredient_only
|
||||||
|
|
||||||
recipe_scale = 10
|
recipe_scale = 10
|
||||||
payload = {"recipeIncrementQuantity": recipe_scale}
|
payload = utils.jsonify(
|
||||||
|
[ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id, recipe_increment_quantity=recipe_scale).model_dump()]
|
||||||
|
)
|
||||||
|
|
||||||
# first add a bunch of quantity to the list
|
# first add a bunch of quantity to the list
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||||
headers=unique_user.token,
|
headers=unique_user.token,
|
||||||
json=payload,
|
json=payload,
|
||||||
)
|
)
|
||||||
|
@ -757,13 +929,15 @@ def test_recipe_manipulation_with_zero_quantities(
|
||||||
|
|
||||||
# add the recipe to the list twice and make sure the quantity is still zero
|
# add the recipe to the list twice and make sure the quantity is still zero
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
|
api_routes.households_shopping_lists_item_id_recipe(shopping_list.id),
|
||||||
|
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||||
headers=unique_user.token,
|
headers=unique_user.token,
|
||||||
)
|
)
|
||||||
utils.assert_deserialize(response, 200)
|
utils.assert_deserialize(response, 200)
|
||||||
|
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
|
api_routes.households_shopping_lists_item_id_recipe(shopping_list.id),
|
||||||
|
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||||
headers=unique_user.token,
|
headers=unique_user.token,
|
||||||
)
|
)
|
||||||
utils.assert_deserialize(response, 200)
|
utils.assert_deserialize(response, 200)
|
||||||
|
|
|
@ -390,6 +390,11 @@ def households_shopping_lists_item_id_label_settings(item_id):
|
||||||
return f"{prefix}/households/shopping/lists/{item_id}/label-settings"
|
return f"{prefix}/households/shopping/lists/{item_id}/label-settings"
|
||||||
|
|
||||||
|
|
||||||
|
def households_shopping_lists_item_id_recipe(item_id):
|
||||||
|
"""`/api/households/shopping/lists/{item_id}/recipe`"""
|
||||||
|
return f"{prefix}/households/shopping/lists/{item_id}/recipe"
|
||||||
|
|
||||||
|
|
||||||
def households_shopping_lists_item_id_recipe_recipe_id(item_id, recipe_id):
|
def households_shopping_lists_item_id_recipe_recipe_id(item_id, recipe_id):
|
||||||
"""`/api/households/shopping/lists/{item_id}/recipe/{recipe_id}`"""
|
"""`/api/households/shopping/lists/{item_id}/recipe/{recipe_id}`"""
|
||||||
return f"{prefix}/households/shopping/lists/{item_id}/recipe/{recipe_id}"
|
return f"{prefix}/households/shopping/lists/{item_id}/recipe/{recipe_id}"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue