pytest shopping list from recipe methods
This commit is contained in:
parent
ab968f225b
commit
262387da3e
@ -58,6 +58,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
|
||||
|
||||
if type(servings) not in [int, float]:
|
||||
servings = getattr(mealplan, 'servings', 1.0)
|
||||
servings_factor = servings / r.servings
|
||||
|
||||
shared_users = list(created_by.get_shopping_share())
|
||||
shared_users.append(created_by)
|
||||
@ -75,7 +76,44 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
|
||||
else:
|
||||
ingredients = Ingredient.objects.filter(step__recipe=r, space=space)
|
||||
|
||||
add_ingredients = ingredients.values_list('id', flat=True)
|
||||
if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand:
|
||||
ingredients = ingredients.exclude(food__on_hand=True)
|
||||
|
||||
related_step_ing = []
|
||||
if related := created_by.userpreference.mealplan_autoinclude_related:
|
||||
# TODO: add levels of related recipes to use when auto-adding mealplans
|
||||
related_recipes = r.get_related_recipes()
|
||||
|
||||
for x in related_recipes:
|
||||
# related recipe is a Step serving size is driven by recipe serving size
|
||||
# TODO once Steps can have a serving size this needs to be refactored
|
||||
if exclude_onhand:
|
||||
# if steps are used more than once in a recipe or subrecipe - I don' think this results in the desired behavior
|
||||
related_step_ing += Ingredient.objects.filter(step__recipe=x, food__on_hand=False, space=space).values_list('id', flat=True)
|
||||
else:
|
||||
related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).values_list('id', flat=True)
|
||||
|
||||
x_ing = []
|
||||
if ingredients.filter(food__recipe=x).exists():
|
||||
for ing in ingredients.filter(food__recipe=x):
|
||||
if exclude_onhand:
|
||||
x_ing = Ingredient.objects.filter(step__recipe=x, food__on_hand=False, space=space)
|
||||
else:
|
||||
x_ing = Ingredient.objects.filter(step__recipe=x, space=space)
|
||||
for i in [x for x in x_ing if not x.food.ignore_shopping]:
|
||||
ShoppingListEntry.objects.create(
|
||||
list_recipe=list_recipe,
|
||||
food=i.food,
|
||||
unit=i.unit,
|
||||
ingredient=i,
|
||||
amount=i.amount * Decimal(servings_factor),
|
||||
created_by=created_by,
|
||||
space=space,
|
||||
)
|
||||
# dont' add food to the shopping list that are actually recipes that will be added as ingredients
|
||||
ingredients = ingredients.exclude(food__recipe=x)
|
||||
|
||||
add_ingredients = list(ingredients.values_list('id', flat=True)) + related_step_ing
|
||||
if not append:
|
||||
existing_list = ShoppingListEntry.objects.filter(list_recipe=list_recipe)
|
||||
# delete shopping list entries not included in ingredients
|
||||
@ -87,7 +125,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
|
||||
# if servings have changed, update the ShoppingListRecipe and existing Entrys
|
||||
if servings <= 0:
|
||||
servings = 1
|
||||
servings_factor = servings / r.servings
|
||||
|
||||
if not created and list_recipe.servings != servings:
|
||||
update_ingredients = set(ingredients.values_list('id', flat=True)) - set(add_ingredients.values_list('id', flat=True))
|
||||
for sle in ShoppingListEntry.objects.filter(list_recipe=list_recipe, ingredient__id__in=update_ingredients):
|
||||
|
@ -106,35 +106,5 @@ def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs
|
||||
'created_by': user,
|
||||
'servings': instance.servings
|
||||
}
|
||||
recipe_ingredients = Ingredient.objects.filter(step__recipe=instance.recipe, space=space)
|
||||
if exclude_onhand := user.userpreference.mealplan_autoexclude_onhand:
|
||||
recipe_ingredients = recipe_ingredients.exclude(food__on_hand=True)
|
||||
if related := user.userpreference.mealplan_autoinclude_related:
|
||||
# TODO: add levels of related recipes to use when auto-adding mealplans
|
||||
related_recipes = instance.recipe.get_related_recipes()
|
||||
# dont' add recipes that are going to have their recipes added to the shopping list
|
||||
kwargs['ingredients'] = recipe_ingredients.exclude(food__recipe__in=related_recipes).values_list('id', flat=True)
|
||||
else:
|
||||
kwargs['ingredients'] = recipe_ingredients.values_list('id', flat=True)
|
||||
|
||||
list_recipe = list_from_recipe(**kwargs)
|
||||
if related:
|
||||
servings_factor = Decimal(instance.servings / instance.recipe.servings)
|
||||
kwargs['list_recipe'] = list_recipe
|
||||
food_recipes = recipe_ingredients.filter(food__recipe__in=related_recipes).values('food__recipe', 'amount')
|
||||
|
||||
for recipe in related_recipes:
|
||||
kwargs['ingredients'] = []
|
||||
if exclude_onhand:
|
||||
kwargs['ingredients'] = Ingredient.objects.filter(step__recipe=recipe, food__on_hand=False, space=space).values_list('id', flat=True)
|
||||
kwargs['recipe'] = recipe
|
||||
|
||||
# assume related recipes are intended to be 'full sized' to parent recipe
|
||||
# Recipe1 (servings:4) includes StepRecipe2(servings:2) a Meal Plan serving size of 8 would assume 4 servings of StepRecipe2
|
||||
if recipe.id in [x['food__recipe'] for x in food_recipes if x['food__recipe'] == recipe.id]:
|
||||
kwargs['servings'] = Decimal(recipe.servings) * sum([x['amount'] for x in food_recipes if x['food__recipe'] == recipe.id]) * servings_factor
|
||||
else:
|
||||
# TODO: When modifying step recipes to allow serving size - will need to update this
|
||||
kwargs['servings'] = Decimal(recipe.servings) * servings_factor
|
||||
|
||||
list_from_recipe(**kwargs, append=True)
|
||||
|
@ -11,7 +11,7 @@ from django_scopes import scopes_disabled
|
||||
from pytest_factoryboy import LazyFixture, register
|
||||
|
||||
from cookbook.models import ShoppingListEntry
|
||||
from cookbook.tests.factories import FoodFactory, ShoppingListEntryFactory
|
||||
from cookbook.tests.factories import ShoppingListEntryFactory
|
||||
|
||||
LIST_URL = 'api:shoppinglistentry-list'
|
||||
DETAIL_URL = 'api:shoppinglistentry-detail'
|
||||
@ -219,11 +219,6 @@ def test_recent(sle, u1_s1):
|
||||
assert [x['checked'] for x in r].count(False) == 9
|
||||
|
||||
|
||||
# TODO test create shopping list from recipe
|
||||
# TODO test delete shopping list from recipe - include created by, shared with and not shared with
|
||||
# TODO test create shopping list from food
|
||||
# TODO test delete shopping list from food - include created by, shared with and not shared with
|
||||
# TODO test create shopping list from mealplan
|
||||
# TODO test create shopping list from recipe, excluding ingredients
|
||||
|
||||
# test delay
|
||||
|
88
cookbook/tests/api/test_api_shopping_recipe.py
Normal file
88
cookbook/tests/api/test_api_shopping_recipe.py
Normal file
@ -0,0 +1,88 @@
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
import factory
|
||||
import pytest
|
||||
from django.contrib import auth
|
||||
from django.forms import model_to_dict
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django_scopes import scopes_disabled
|
||||
from pytest_factoryboy import LazyFixture, register
|
||||
|
||||
from cookbook.models import ShoppingListEntry
|
||||
from cookbook.tests.factories import RecipeFactory
|
||||
|
||||
SHOPPING_LIST_URL = 'api:shoppinglistentry-list'
|
||||
SHOPPING_RECIPE_URL = 'api:recipe-shopping'
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def recipe(request, space_1, u1_s1):
|
||||
try:
|
||||
params = request.param # request.param is a magic variable
|
||||
except AttributeError:
|
||||
params = {}
|
||||
step_recipe = params.get('steps__count', 1)
|
||||
steps__recipe_count = params.get('steps__recipe_count', 0)
|
||||
steps__food_recipe_count = params.get('steps__food_recipe_count', {})
|
||||
created_by = params.get('created_by', auth.get_user(u1_s1))
|
||||
|
||||
return RecipeFactory.create(
|
||||
steps__recipe_count=steps__recipe_count,
|
||||
steps__food_recipe_count=steps__food_recipe_count,
|
||||
created_by=created_by,
|
||||
space=space_1,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['g1_s1', 204],
|
||||
['u1_s1', 204],
|
||||
['u1_s2', 404],
|
||||
['a1_s1', 204],
|
||||
])
|
||||
@pytest.mark.parametrize("recipe, sle_count", [
|
||||
({}, 10),
|
||||
({'steps__recipe_count': 1}, 20), # shopping list from recipe with StepRecipe
|
||||
({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 19), # shopping list from recipe with food recipe
|
||||
({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}, 29), # shopping list from recipe with StepRecipe and food recipe
|
||||
], indirect=['recipe'])
|
||||
def test_shopping_recipe_method(request, arg, recipe, sle_count, u1_s1, u2_s1):
|
||||
c = request.getfixturevalue(arg[0])
|
||||
user = auth.get_user(c)
|
||||
user.userpreference.mealplan_autoadd_shopping = True
|
||||
user.userpreference.save()
|
||||
|
||||
assert len(json.loads(c.get(reverse(SHOPPING_LIST_URL)).content)) == 0
|
||||
|
||||
url = reverse(SHOPPING_RECIPE_URL, args={recipe.id})
|
||||
r = c.put(url)
|
||||
assert r.status_code == arg[1]
|
||||
# only PUT method should work
|
||||
if r.status_code == 204: # skip anonymous user
|
||||
|
||||
r = json.loads(c.get(reverse(SHOPPING_LIST_URL)).content)
|
||||
assert len(r) == sle_count # recipe factory creates 10 ingredients by default
|
||||
assert [x['created_by']['id'] for x in r].count(user.id) == sle_count
|
||||
# user in space can't see shopping list
|
||||
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
|
||||
user.userpreference.shopping_share.add(auth.get_user(u2_s1))
|
||||
# after share, user in space can see shopping list
|
||||
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
|
||||
# confirm that the author of the recipe doesn't have access to shopping list
|
||||
if c != u1_s1:
|
||||
assert len(json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
|
||||
|
||||
r = c.get(url)
|
||||
assert r.status_code == 405
|
||||
r = c.post(url)
|
||||
assert r.status_code == 405
|
||||
r = c.delete(url)
|
||||
assert r.status_code == 405
|
||||
|
||||
|
||||
# TODO test creating shopping list from recipe that includes recipes from multiple users
|
||||
# TODO test create shopping list from recipe, excluding ingredients
|
||||
# TODO meal plan recipe with all the user preferences tested
|
||||
# TODO shopping list from recipe with different servings
|
@ -9,6 +9,8 @@ from django_scopes import scopes_disabled
|
||||
from faker import Factory as FakerFactory
|
||||
from pytest_factoryboy import register
|
||||
|
||||
from cookbook.models import Step
|
||||
|
||||
# this code will run immediately prior to creating the model object useful when you want a reverse relationship
|
||||
# log = factory.RelatedFactory(
|
||||
# UserLogFactory,
|
||||
@ -138,15 +140,21 @@ class KeywordFactory(factory.django.DjangoModelFactory):
|
||||
# icon = models.CharField(max_length=16, blank=True, null=True)
|
||||
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
num = None # used on upstream factories to generate num keywords
|
||||
|
||||
class Params:
|
||||
num = None
|
||||
|
||||
class Meta:
|
||||
model = 'cookbook.Keyword'
|
||||
django_get_or_create = ('name', 'space',)
|
||||
exclude = ('num')
|
||||
|
||||
|
||||
@register
|
||||
class IngredientFactory(factory.django.DjangoModelFactory):
|
||||
"""Ingredient factory."""
|
||||
# TODO add optional recipe food
|
||||
food = factory.SubFactory(FoodFactory, space=factory.SelfAttribute('..space'))
|
||||
unit = factory.SubFactory(UnitFactory, space=factory.SelfAttribute('..space'))
|
||||
amount = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=10))
|
||||
@ -203,6 +211,7 @@ class ShoppingListRecipeFactory(factory.django.DjangoModelFactory):
|
||||
)
|
||||
servings = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=10))
|
||||
mealplan = factory.SubFactory(MealPlanFactory, space=factory.SelfAttribute('..space'))
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
|
||||
class Params:
|
||||
has_recipe = False
|
||||
@ -242,26 +251,43 @@ class ShoppingListEntryFactory(factory.django.DjangoModelFactory):
|
||||
@register
|
||||
class StepFactory(factory.django.DjangoModelFactory):
|
||||
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5))
|
||||
# type = models.CharField(
|
||||
# choices=((TEXT, _('Text')), (TIME, _('Time')), (FILE, _('File')), (RECIPE, _('Recipe')),),
|
||||
# default=TEXT,
|
||||
# max_length=16
|
||||
# )
|
||||
instruction = factory.LazyAttribute(lambda x: ''.join(faker.paragraphs(nb=5)))
|
||||
ingredients = factory.SubFactory(IngredientFactory, space=factory.SelfAttribute('..space'))
|
||||
# TODO add optional recipe food, make dependent on recipe, make number of recipes a Params
|
||||
ingredients__count = 10 # default number of ingredients to add
|
||||
time = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=1000))
|
||||
order = 0
|
||||
order = factory.Sequence(lambda x: x)
|
||||
# file = models.ForeignKey('UserFile', on_delete=models.PROTECT, null=True, blank=True)
|
||||
show_as_header = True
|
||||
step_recipe = factory.Maybe(
|
||||
factory.LazyAttribute(lambda x: x.has_recipe),
|
||||
yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')),
|
||||
no_declaration=None
|
||||
)
|
||||
step_recipe__has_recipe = False
|
||||
ingredients__food_recipe_count = 0
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
|
||||
class Params:
|
||||
has_recipe = False
|
||||
@factory.post_generation
|
||||
def step_recipe(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
if kwargs.get('has_recipe', False):
|
||||
step_recipe = RecipeFactory(space=self.space)
|
||||
self.type = Step.RECIPE
|
||||
|
||||
@factory.post_generation
|
||||
def ingredients(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
num_ing = kwargs.get('count', 0)
|
||||
num_food_recipe = kwargs.get('food_recipe_count', 0)
|
||||
if num_ing > 0:
|
||||
for i in range(num_ing):
|
||||
if num_food_recipe > 0:
|
||||
has_recipe = True
|
||||
num_food_recipe = num_food_recipe-1
|
||||
else:
|
||||
has_recipe = False
|
||||
self.ingredients.add(IngredientFactory(space=self.space, food__has_recipe=has_recipe))
|
||||
elif extracted:
|
||||
for ing in extracted:
|
||||
self.ingredients.add(ing)
|
||||
|
||||
class Meta:
|
||||
model = 'cookbook.Step'
|
||||
@ -272,8 +298,54 @@ class RecipeFactory(factory.django.DjangoModelFactory):
|
||||
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=7))
|
||||
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
|
||||
servings = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=20))
|
||||
servings_text = factory.LazyAttribute(lambda x: faker.sentence(nb_words=1))
|
||||
# image = models.ImageField(upload_to='recipes/', blank=True, null=True) #TODO test recipe image api
|
||||
servings_text = factory.LazyAttribute(lambda x: faker.sentence(nb_words=1)) # TODO generate list of expected servings text that can be iterated through
|
||||
keywords__count = 5 # default number of keywords to generate
|
||||
steps__count = 1 # default number of steps to create
|
||||
steps__recipe_count = 0 # default number of step recipes to create
|
||||
steps__food_recipe_count = {} # by default, don't create food recipes, to override {'steps__food_recipe_count': {'step': 0, 'count': 1}}
|
||||
working_time = factory.LazyAttribute(lambda x: faker.random_int(min=0, max=360))
|
||||
waiting_time = factory.LazyAttribute(lambda x: faker.random_int(min=0, max=360))
|
||||
internal = False
|
||||
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
|
||||
created_at = factory.LazyAttribute(lambda x: faker.date_this_decade())
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
|
||||
@factory.post_generation
|
||||
def keywords(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
# Simple build, do nothing.
|
||||
return
|
||||
|
||||
num_kw = kwargs.get('count', 0)
|
||||
if num_kw > 0:
|
||||
for i in range(num_kw):
|
||||
self.keywords.add(KeywordFactory(space=self.space))
|
||||
elif extracted:
|
||||
for kw in extracted:
|
||||
self.keywords.add(kw)
|
||||
|
||||
@factory.post_generation
|
||||
def steps(self, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
|
||||
food_recipe_count = kwargs.get('food_recipe_count', {}) # TODO - pass this
|
||||
num_steps = kwargs.get('count', 0)
|
||||
num_recipe_steps = kwargs.get('recipe_count', 0)
|
||||
if num_steps > 0:
|
||||
for i in range(num_steps):
|
||||
ing_recipe_count = 0
|
||||
if food_recipe_count.get('step', None) == i:
|
||||
ing_recipe_count = food_recipe_count.get('count', 0)
|
||||
self.steps.add(StepFactory(space=self.space, ingredients__food_recipe_count=ing_recipe_count))
|
||||
if num_recipe_steps > 0:
|
||||
for j in range(num_recipe_steps):
|
||||
self.steps.add(StepFactory(space=self.space, step_recipe__has_recipe=True))
|
||||
if extracted and (num_steps + num_recipe_steps == 0):
|
||||
for step in extracted:
|
||||
self.steps.add(step)
|
||||
|
||||
# image = models.ImageField(upload_to='recipes/', blank=True, null=True) #TODO test recipe image api https://factoryboy.readthedocs.io/en/stable/orms.html#factory.django.ImageField
|
||||
# storage = models.ForeignKey(
|
||||
# Storage, on_delete=models.PROTECT, blank=True, null=True
|
||||
# )
|
||||
@ -281,18 +353,8 @@ class RecipeFactory(factory.django.DjangoModelFactory):
|
||||
# file_path = models.CharField(max_length=512, default="", blank=True)
|
||||
# link = models.CharField(max_length=512, null=True, blank=True)
|
||||
# cors_link = models.CharField(max_length=1024, null=True, blank=True)
|
||||
keywords = factory.SubFactory(KeywordFactory, space=factory.SelfAttribute('..space'))
|
||||
steps = factory.SubFactory(StepFactory, space=factory.SelfAttribute('..space'))
|
||||
working_time = factory.LazyAttribute(lambda x: faker.random_int(min=0, max=360))
|
||||
waiting_time = factory.LazyAttribute(lambda x: faker.random_int(min=0, max=360))
|
||||
internal = False
|
||||
# nutrition = models.ForeignKey(
|
||||
# NutritionInformation, blank=True, null=True, on_delete=models.CASCADE
|
||||
# )
|
||||
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
|
||||
created_at = factory.LazyAttribute(lambda x: faker.date_this_decade())
|
||||
# nutrition = models.ForeignKey(NutritionInformation, blank=True, null=True, on_delete=models.CASCADE)
|
||||
# updated_at = models.DateTimeField(auto_now=True)
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
|
||||
class Meta:
|
||||
model = 'cookbook.Recipe'
|
||||
|
@ -412,7 +412,8 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
permission_classes = [CustomIsUser]
|
||||
pagination_class = DefaultPagination
|
||||
|
||||
@decorators.action(detail=True, methods=['PUT'], serializer_class=FoodShoppingUpdateSerializer,)
|
||||
''
|
||||
@decorators.action(detail=True, methods=['PUT'], serializer_class=FoodShoppingUpdateSerializer,)
|
||||
def shopping(self, request, pk):
|
||||
obj = self.get_object()
|
||||
shared_users = list(self.request.user.get_shopping_share())
|
||||
@ -594,6 +595,11 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
schema = QueryParamAutoSchema()
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if self.detail:
|
||||
self.queryset = self.queryset.filter(space=self.request.space)
|
||||
return super().get_queryset()
|
||||
|
||||
share = self.request.query_params.get('share', None)
|
||||
if not (share and self.detail):
|
||||
self.queryset = self.queryset.filter(space=self.request.space)
|
||||
|
Loading…
Reference in New Issue
Block a user