mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-05 06:08:46 -05:00
Merge pull request #1447 from smilerz/mealplan_shopping_useability
Mealplan shopping useability
This commit is contained in:
@@ -205,9 +205,9 @@ class CustomIsShared(permissions.BasePermission):
|
||||
return request.user.is_authenticated
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
# temporary hack to make old shopping list work with new shopping list
|
||||
if obj.__class__.__name__ in ['ShoppingList', 'ShoppingListEntry']:
|
||||
return is_object_shared(request.user, obj) or obj.created_by in list(request.user.get_shopping_share())
|
||||
# # temporary hack to make old shopping list work with new shopping list
|
||||
# if obj.__class__.__name__ in ['ShoppingList', 'ShoppingListEntry']:
|
||||
# return is_object_shared(request.user, obj) or obj.created_by in list(request.user.get_shopping_share())
|
||||
return is_object_shared(request.user, obj)
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.helper.HelperFunctions import Round, str2bool
|
||||
from cookbook.models import (Ingredient, ShoppingListEntry, ShoppingListRecipe,
|
||||
from cookbook.models import (Ingredient, MealPlan, Recipe, ShoppingListEntry, ShoppingListRecipe,
|
||||
SupermarketCategoryRelation)
|
||||
from recipes import settings
|
||||
|
||||
@@ -38,118 +38,272 @@ def shopping_helper(qs, request):
|
||||
return qs.order_by(*supermarket_order).select_related('unit', 'food', 'ingredient', 'created_by', 'list_recipe', 'list_recipe__mealplan', 'list_recipe__recipe')
|
||||
|
||||
|
||||
# TODO refactor as class
|
||||
def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None, ingredients=None, created_by=None, space=None, append=False):
|
||||
"""
|
||||
Creates ShoppingListRecipe and associated ShoppingListEntrys from a recipe or a meal plan with a recipe
|
||||
:param list_recipe: Modify an existing ShoppingListRecipe
|
||||
:param recipe: Recipe to use as list of ingredients. One of [recipe, mealplan] are required
|
||||
:param mealplan: alternatively use a mealplan recipe as source of ingredients
|
||||
:param servings: Optional: Number of servings to use to scale shoppinglist. If servings = 0 an existing recipe list will be deleted
|
||||
:param ingredients: Ingredients, list of ingredient IDs to include on the shopping list. When not provided all ingredients will be used
|
||||
:param append: If False will remove any entries not included with ingredients, when True will append ingredients to the shopping list
|
||||
"""
|
||||
r = recipe or getattr(mealplan, 'recipe', None) or getattr(list_recipe, 'recipe', None)
|
||||
if not r:
|
||||
raise ValueError(_("You must supply a recipe or mealplan"))
|
||||
class RecipeShoppingEditor():
|
||||
def __init__(self, user, space, **kwargs):
|
||||
self.created_by = user
|
||||
self.space = space
|
||||
self._kwargs = {**kwargs}
|
||||
|
||||
created_by = created_by or getattr(ShoppingListEntry.objects.filter(list_recipe=list_recipe).first(), 'created_by', None)
|
||||
if not created_by:
|
||||
raise ValueError(_("You must supply a created_by"))
|
||||
self.mealplan = self._kwargs.get('mealplan', None)
|
||||
if type(self.mealplan) in [int, float]:
|
||||
self.mealplan = MealPlan.objects.filter(id=self.mealplan, space=self.space)
|
||||
self.id = self._kwargs.get('id', None)
|
||||
|
||||
try:
|
||||
servings = float(servings)
|
||||
except (ValueError, TypeError):
|
||||
servings = getattr(mealplan, 'servings', 1.0)
|
||||
self._shopping_list_recipe = self.get_shopping_list_recipe(self.id, self.created_by, self.space)
|
||||
|
||||
servings_factor = servings / r.servings
|
||||
if self._shopping_list_recipe:
|
||||
# created_by needs to be sticky to original creator as it is 'their' shopping list
|
||||
# changing shopping list created_by can shift some items to new owner which may not share in the other direction
|
||||
self.created_by = getattr(self._shopping_list_recipe.entries.first(), 'created_by', self.created_by)
|
||||
|
||||
shared_users = list(created_by.get_shopping_share())
|
||||
shared_users.append(created_by)
|
||||
if list_recipe:
|
||||
created = False
|
||||
else:
|
||||
list_recipe = ShoppingListRecipe.objects.create(recipe=r, mealplan=mealplan, servings=servings)
|
||||
created = True
|
||||
self.recipe = getattr(self._shopping_list_recipe, 'recipe', None) or self._kwargs.get('recipe', None) or getattr(self.mealplan, 'recipe', None)
|
||||
if type(self.recipe) in [int, float]:
|
||||
self.recipe = Recipe.objects.filter(id=self.recipe, space=self.space)
|
||||
|
||||
related_step_ing = []
|
||||
if servings == 0 and not created:
|
||||
list_recipe.delete()
|
||||
return []
|
||||
elif ingredients:
|
||||
ingredients = Ingredient.objects.filter(pk__in=ingredients, space=space)
|
||||
else:
|
||||
ingredients = Ingredient.objects.filter(step__recipe=r, food__ignore_shopping=False, space=space)
|
||||
try:
|
||||
self.servings = float(self._kwargs.get('servings', None))
|
||||
except (ValueError, TypeError):
|
||||
self.servings = getattr(self._shopping_list_recipe, 'servings', None) or getattr(self.mealplan, 'servings', None) or getattr(self.recipe, 'servings', None)
|
||||
|
||||
if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand:
|
||||
ingredients = ingredients.exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
||||
@property
|
||||
def _servings_factor(self):
|
||||
return self.servings / self.recipe.servings
|
||||
|
||||
if related := created_by.userpreference.mealplan_autoinclude_related:
|
||||
# TODO: add levels of related recipes (related recipes of related recipes) to use when auto-adding mealplans
|
||||
related_recipes = r.get_related_recipes()
|
||||
@property
|
||||
def _shared_users(self):
|
||||
return [*list(self.created_by.get_shopping_share()), self.created_by]
|
||||
|
||||
for x in related_recipes:
|
||||
# related recipe is a Step serving size is driven by recipe serving size
|
||||
# TODO once/if 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, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users]).values_list('id', flat=True)
|
||||
else:
|
||||
related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).values_list('id', flat=True)
|
||||
@staticmethod
|
||||
def get_shopping_list_recipe(id, user, space):
|
||||
return ShoppingListRecipe.objects.filter(id=id).filter(Q(shoppinglist__space=space) | Q(entries__space=space)).filter(
|
||||
Q(shoppinglist__created_by=user)
|
||||
| Q(shoppinglist__shared=user)
|
||||
| Q(entries__created_by=user)
|
||||
| Q(entries__created_by__in=list(user.get_shopping_share()))
|
||||
).prefetch_related('entries').first()
|
||||
|
||||
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__ignore_shopping=False, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
||||
else:
|
||||
x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__ignore_shopping=True)
|
||||
for i in [x for x in x_ing]:
|
||||
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)
|
||||
def get_recipe_ingredients(self, id, exclude_onhand=False):
|
||||
if exclude_onhand:
|
||||
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space).exclude(food__onhand_users__id__in=[x.id for x in self._shared_users])
|
||||
else:
|
||||
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space)
|
||||
|
||||
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
|
||||
existing_list.exclude(ingredient__in=ingredients).delete()
|
||||
# add shopping list entries that did not previously exist
|
||||
add_ingredients = set(add_ingredients) - set(existing_list.values_list('ingredient__id', flat=True))
|
||||
add_ingredients = Ingredient.objects.filter(id__in=add_ingredients, space=space)
|
||||
@property
|
||||
def _include_related(self):
|
||||
return self.created_by.userpreference.mealplan_autoinclude_related
|
||||
|
||||
# if servings have changed, update the ShoppingListRecipe and existing Entries
|
||||
if servings <= 0:
|
||||
servings = 1
|
||||
@property
|
||||
def _exclude_onhand(self):
|
||||
return self.created_by.userpreference.mealplan_autoexclude_onhand
|
||||
|
||||
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))
|
||||
list_recipe.servings = servings
|
||||
list_recipe.save()
|
||||
for sle in ShoppingListEntry.objects.filter(list_recipe=list_recipe, ingredient__id__in=update_ingredients):
|
||||
sle.amount = sle.ingredient.amount * Decimal(servings_factor)
|
||||
def create(self, **kwargs):
|
||||
ingredients = kwargs.get('ingredients', None)
|
||||
exclude_onhand = not ingredients and self._exclude_onhand
|
||||
if servings := kwargs.get('servings', None):
|
||||
self.servings = float(servings)
|
||||
|
||||
if mealplan := kwargs.get('mealplan', None):
|
||||
self.mealplan = mealplan
|
||||
self.recipe = mealplan.recipe
|
||||
elif recipe := kwargs.get('recipe', None):
|
||||
self.recipe = recipe
|
||||
|
||||
if not self.servings:
|
||||
self.servings = getattr(self.mealplan, 'servings', None) or getattr(self.recipe, 'servings', 1.0)
|
||||
|
||||
self._shopping_list_recipe = ShoppingListRecipe.objects.create(recipe=self.recipe, mealplan=self.mealplan, servings=self.servings)
|
||||
|
||||
if ingredients:
|
||||
self._add_ingredients(ingredients=ingredients)
|
||||
else:
|
||||
if self._include_related:
|
||||
related = self.recipe.get_related_recipes()
|
||||
self._add_ingredients(self.get_recipe_ingredients(self.recipe.id, exclude_onhand=exclude_onhand).exclude(food__recipe__in=related))
|
||||
for r in related:
|
||||
self._add_ingredients(self.get_recipe_ingredients(r.id, exclude_onhand=exclude_onhand).exclude(food__recipe__in=related))
|
||||
else:
|
||||
self._add_ingredients(self.get_recipe_ingredients(self.recipe.id, exclude_onhand=exclude_onhand))
|
||||
|
||||
return True
|
||||
|
||||
def add(self, **kwargs):
|
||||
return
|
||||
|
||||
def edit(self, servings=None, ingredients=None, **kwargs):
|
||||
if servings:
|
||||
self.servings = servings
|
||||
|
||||
self._delete_ingredients(ingredients=ingredients)
|
||||
if self.servings != self._shopping_list_recipe.servings:
|
||||
self.edit_servings()
|
||||
self._add_ingredients(ingredients=ingredients)
|
||||
return True
|
||||
|
||||
def edit_servings(self, servings=None, **kwargs):
|
||||
if servings:
|
||||
self.servings = servings
|
||||
if id := kwargs.get('id', None):
|
||||
self._shopping_list_recipe = self.get_shopping_list_recipe(id, self.created_by, self.space)
|
||||
if not self.servings:
|
||||
raise ValueError(_("You must supply a servings size"))
|
||||
|
||||
if self._shopping_list_recipe.servings == self.servings:
|
||||
return True
|
||||
|
||||
for sle in ShoppingListEntry.objects.filter(list_recipe=self._shopping_list_recipe):
|
||||
sle.amount = sle.ingredient.amount * Decimal(self._servings_factor)
|
||||
sle.save()
|
||||
self._shopping_list_recipe.servings = self.servings
|
||||
self._shopping_list_recipe.save()
|
||||
return True
|
||||
|
||||
# add any missing Entries
|
||||
for i in [x for x in add_ingredients if x.food]:
|
||||
def delete(self, **kwargs):
|
||||
try:
|
||||
self._shopping_list_recipe.delete()
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
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,
|
||||
)
|
||||
def _add_ingredients(self, ingredients=None):
|
||||
if not ingredients:
|
||||
return
|
||||
elif type(ingredients) == list:
|
||||
ingredients = Ingredient.objects.filter(id__in=ingredients)
|
||||
existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True)
|
||||
add_ingredients = ingredients.exclude(id__in=existing)
|
||||
|
||||
# return all shopping list items
|
||||
return list_recipe
|
||||
for i in [x for x in add_ingredients if x.food]:
|
||||
ShoppingListEntry.objects.create(
|
||||
list_recipe=self._shopping_list_recipe,
|
||||
food=i.food,
|
||||
unit=i.unit,
|
||||
ingredient=i,
|
||||
amount=i.amount * Decimal(self._servings_factor),
|
||||
created_by=self.created_by,
|
||||
space=self.space,
|
||||
)
|
||||
|
||||
# deletes shopping list entries not in ingredients list
|
||||
def _delete_ingredients(self, ingredients=None):
|
||||
if not ingredients:
|
||||
return
|
||||
to_delete = self._shopping_list_recipe.entries.exclude(ingredient__in=ingredients)
|
||||
ShoppingListEntry.objects.filter(id__in=to_delete).delete()
|
||||
self._shopping_list_recipe = self.get_shopping_list_recipe(self.id, self.created_by, self.space)
|
||||
|
||||
|
||||
# # TODO refactor as class
|
||||
# def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None, ingredients=None, created_by=None, space=None, append=False):
|
||||
# """
|
||||
# Creates ShoppingListRecipe and associated ShoppingListEntrys from a recipe or a meal plan with a recipe
|
||||
# :param list_recipe: Modify an existing ShoppingListRecipe
|
||||
# :param recipe: Recipe to use as list of ingredients. One of [recipe, mealplan] are required
|
||||
# :param mealplan: alternatively use a mealplan recipe as source of ingredients
|
||||
# :param servings: Optional: Number of servings to use to scale shoppinglist. If servings = 0 an existing recipe list will be deleted
|
||||
# :param ingredients: Ingredients, list of ingredient IDs to include on the shopping list. When not provided all ingredients will be used
|
||||
# :param append: If False will remove any entries not included with ingredients, when True will append ingredients to the shopping list
|
||||
# """
|
||||
# r = recipe or getattr(mealplan, 'recipe', None) or getattr(list_recipe, 'recipe', None)
|
||||
# if not r:
|
||||
# raise ValueError(_("You must supply a recipe or mealplan"))
|
||||
|
||||
# created_by = created_by or getattr(ShoppingListEntry.objects.filter(list_recipe=list_recipe).first(), 'created_by', None)
|
||||
# if not created_by:
|
||||
# raise ValueError(_("You must supply a created_by"))
|
||||
|
||||
# try:
|
||||
# servings = float(servings)
|
||||
# except (ValueError, TypeError):
|
||||
# servings = getattr(mealplan, 'servings', 1.0)
|
||||
|
||||
# servings_factor = servings / r.servings
|
||||
|
||||
# shared_users = list(created_by.get_shopping_share())
|
||||
# shared_users.append(created_by)
|
||||
# if list_recipe:
|
||||
# created = False
|
||||
# else:
|
||||
# list_recipe = ShoppingListRecipe.objects.create(recipe=r, mealplan=mealplan, servings=servings)
|
||||
# created = True
|
||||
|
||||
# related_step_ing = []
|
||||
# if servings == 0 and not created:
|
||||
# list_recipe.delete()
|
||||
# return []
|
||||
# elif ingredients:
|
||||
# ingredients = Ingredient.objects.filter(pk__in=ingredients, space=space)
|
||||
# else:
|
||||
# ingredients = Ingredient.objects.filter(step__recipe=r, food__ignore_shopping=False, space=space)
|
||||
|
||||
# if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand:
|
||||
# ingredients = ingredients.exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
||||
|
||||
# if related := created_by.userpreference.mealplan_autoinclude_related:
|
||||
# # TODO: add levels of related recipes (related recipes 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/if 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, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users]).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__ignore_shopping=False, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
||||
# else:
|
||||
# x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__ignore_shopping=True)
|
||||
# for i in [x for x in x_ing]:
|
||||
# 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
|
||||
# existing_list.exclude(ingredient__in=ingredients).delete()
|
||||
# # add shopping list entries that did not previously exist
|
||||
# add_ingredients = set(add_ingredients) - set(existing_list.values_list('ingredient__id', flat=True))
|
||||
# add_ingredients = Ingredient.objects.filter(id__in=add_ingredients, space=space)
|
||||
|
||||
# # if servings have changed, update the ShoppingListRecipe and existing Entries
|
||||
# if servings <= 0:
|
||||
# servings = 1
|
||||
|
||||
# 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))
|
||||
# list_recipe.servings = servings
|
||||
# list_recipe.save()
|
||||
# for sle in ShoppingListEntry.objects.filter(list_recipe=list_recipe, ingredient__id__in=update_ingredients):
|
||||
# sle.amount = sle.ingredient.amount * Decimal(servings_factor)
|
||||
# sle.save()
|
||||
|
||||
# # add any missing Entries
|
||||
# for i in [x for x in add_ingredients if x.food]:
|
||||
|
||||
# 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,
|
||||
# )
|
||||
|
||||
# # return all shopping list items
|
||||
# return list_recipe
|
||||
|
||||
20
cookbook/migrations/0168_add_unit_searchfields.py
Normal file
20
cookbook/migrations/0168_add_unit_searchfields.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from django.db import migrations
|
||||
|
||||
from cookbook.models import SearchFields
|
||||
|
||||
|
||||
def create_searchfields(apps, schema_editor):
|
||||
SearchFields.objects.create(name='Units', field='steps__ingredients__unit__name')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0167_userpreference_left_handed'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
create_searchfields
|
||||
),
|
||||
]
|
||||
@@ -609,7 +609,7 @@ class NutritionInformation(models.Model, PermissionModelMixin):
|
||||
)
|
||||
proteins = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||
calories = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||
source = models.CharField( max_length=512, default="", null=True, blank=True)
|
||||
source = models.CharField(max_length=512, default="", null=True, blank=True)
|
||||
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
@@ -852,11 +852,12 @@ class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), model
|
||||
def __str__(self):
|
||||
return f'Shopping list entry {self.id}'
|
||||
|
||||
# TODO deprecate
|
||||
def get_shared(self):
|
||||
return self.shoppinglist_set.first().shared.all()
|
||||
try:
|
||||
return self.shoppinglist_set.first().shared.all()
|
||||
except AttributeError:
|
||||
return self.created_by.userpreference.shopping_share.all()
|
||||
|
||||
# TODO deprecate
|
||||
def get_owner(self):
|
||||
try:
|
||||
return self.created_by or self.shoppinglist_set.first().created_by
|
||||
@@ -881,6 +882,12 @@ class ShoppingList(ExportModelOperationsMixin('shopping_list'), models.Model, Pe
|
||||
def __str__(self):
|
||||
return f'Shopping list {self.id}'
|
||||
|
||||
def get_shared(self):
|
||||
try:
|
||||
return self.shared.all() or self.created_by.userpreference.shopping_share.all()
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
|
||||
class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, PermissionModelMixin):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||
|
||||
@@ -13,7 +13,7 @@ from rest_framework.exceptions import NotFound, ValidationError
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from cookbook.helper.HelperFunctions import str2bool
|
||||
from cookbook.helper.shopping_helper import list_from_recipe
|
||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor
|
||||
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Food,
|
||||
FoodInheritField, ImportLog, Ingredient, Keyword, MealPlan, MealType,
|
||||
NutritionInformation, Recipe, RecipeBook, RecipeBookEntry,
|
||||
@@ -660,7 +660,8 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
||||
validated_data['created_by'] = self.context['request'].user
|
||||
mealplan = super().create(validated_data)
|
||||
if self.context['request'].data.get('addshopping', False):
|
||||
list_from_recipe(mealplan=mealplan, servings=validated_data['servings'], created_by=validated_data['created_by'], space=validated_data['space'])
|
||||
SLR = RecipeShoppingEditor(user=validated_data['created_by'], space=validated_data['space'])
|
||||
SLR.create(mealplan=mealplan, servings=validated_data['servings'])
|
||||
return mealplan
|
||||
|
||||
class Meta:
|
||||
@@ -694,12 +695,8 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||
def update(self, instance, validated_data):
|
||||
# TODO remove once old shopping list
|
||||
if 'servings' in validated_data and self.context.get('view', None).__class__.__name__ != 'ShoppingListViewSet':
|
||||
list_from_recipe(
|
||||
list_recipe=instance,
|
||||
servings=validated_data['servings'],
|
||||
created_by=self.context['request'].user,
|
||||
space=self.context['request'].space
|
||||
)
|
||||
SLR = RecipeShoppingEditor(user=self.context['request'].user, space=self.context['request'].space)
|
||||
SLR.edit_servings(servings=validated_data['servings'], id=instance.id)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -6,8 +6,9 @@ from django.contrib.postgres.search import SearchVector
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import translation
|
||||
from django_scopes import scope
|
||||
|
||||
from cookbook.helper.shopping_helper import list_from_recipe
|
||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor
|
||||
from cookbook.managers import DICTIONARY
|
||||
from cookbook.models import (Food, FoodInheritField, Ingredient, MealPlan, Recipe,
|
||||
ShoppingListEntry, Step)
|
||||
@@ -104,20 +105,31 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs):
|
||||
|
||||
@receiver(post_save, sender=MealPlan)
|
||||
def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs):
|
||||
if not instance:
|
||||
return
|
||||
user = instance.get_owner()
|
||||
if not user.userpreference.mealplan_autoadd_shopping:
|
||||
with scope(space=instance.space):
|
||||
slr_exists = instance.shoppinglistrecipe_set.exists()
|
||||
|
||||
if not created and slr_exists:
|
||||
for x in instance.shoppinglistrecipe_set.all():
|
||||
# assuming that permissions checks for the MealPlan have happened upstream
|
||||
if instance.servings != x.servings:
|
||||
SLR = RecipeShoppingEditor(id=x.id, user=user, space=instance.space)
|
||||
SLR.edit_servings(servings=instance.servings)
|
||||
# list_recipe = list_from_recipe(list_recipe=x, servings=instance.servings, space=instance.space)
|
||||
elif not user.userpreference.mealplan_autoadd_shopping or not instance.recipe:
|
||||
return
|
||||
|
||||
if not created and instance.shoppinglistrecipe_set.exists():
|
||||
for x in instance.shoppinglistrecipe_set.all():
|
||||
if instance.servings != x.servings:
|
||||
list_recipe = list_from_recipe(list_recipe=x, servings=instance.servings, space=instance.space)
|
||||
elif created:
|
||||
if created:
|
||||
# if creating a mealplan - perform shopping list activities
|
||||
kwargs = {
|
||||
'mealplan': instance,
|
||||
'space': instance.space,
|
||||
'created_by': user,
|
||||
'servings': instance.servings
|
||||
}
|
||||
list_recipe = list_from_recipe(**kwargs)
|
||||
# kwargs = {
|
||||
# 'mealplan': instance,
|
||||
# 'space': instance.space,
|
||||
# 'created_by': user,
|
||||
# 'servings': instance.servings
|
||||
# }
|
||||
SLR = RecipeShoppingEditor(user=user, space=instance.space)
|
||||
SLR.create(mealplan=instance, servings=instance.servings)
|
||||
|
||||
# list_recipe = list_from_recipe(**kwargs)
|
||||
|
||||
@@ -156,6 +156,32 @@ def test_sharing(request, shared, count, sle_2, sle, u1_s1):
|
||||
# confirm shared user sees their list and the list that's shared with them
|
||||
assert len(json.loads(r.content)) == count
|
||||
|
||||
# test shared user can mark complete
|
||||
x = shared_client.patch(
|
||||
reverse(DETAIL_URL, args={sle[0].id}),
|
||||
{'checked': True},
|
||||
content_type='application/json'
|
||||
)
|
||||
r = json.loads(shared_client.get(reverse(LIST_URL)).content)
|
||||
assert len(r) == count
|
||||
# count unchecked entries
|
||||
if not x.status_code == 404:
|
||||
count = count-1
|
||||
assert [x['checked'] for x in r].count(False) == count
|
||||
# test shared user can delete
|
||||
x = shared_client.delete(
|
||||
reverse(
|
||||
DETAIL_URL,
|
||||
args={sle[1].id}
|
||||
)
|
||||
)
|
||||
r = json.loads(shared_client.get(reverse(LIST_URL)).content)
|
||||
assert len(r) == count
|
||||
# count unchecked entries
|
||||
if not x.status_code == 404:
|
||||
count = count-1
|
||||
assert [x['checked'] for x in r].count(False) == count
|
||||
|
||||
|
||||
def test_completed(sle, u1_s1):
|
||||
# check 1 entry
|
||||
|
||||
@@ -164,7 +164,7 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u
|
||||
assert len(r) == sle_count
|
||||
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
|
||||
|
||||
# test removing 2 items from shopping list
|
||||
# test removing 3 items from shopping list
|
||||
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}),
|
||||
{'list_recipe': list_recipe, 'ingredients': keep_ing},
|
||||
content_type='application/json'
|
||||
|
||||
@@ -41,7 +41,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, Cus
|
||||
from cookbook.helper.recipe_html_import import get_recipe_from_source
|
||||
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch, old_search
|
||||
from cookbook.helper.recipe_url_import import get_from_scraper
|
||||
from cookbook.helper.shopping_helper import list_from_recipe, shopping_helper
|
||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
|
||||
from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, FoodInheritField,
|
||||
ImportLog, Ingredient, Keyword, MealPlan, MealType, Recipe, RecipeBook,
|
||||
RecipeBookEntry, ShareLink, ShoppingList, ShoppingListEntry,
|
||||
@@ -153,11 +153,15 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
||||
)
|
||||
else:
|
||||
# TODO have this check unaccent search settings or other search preferences?
|
||||
filter = Q(name__icontains=query)
|
||||
if any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
||||
filter |= Q(name__unaccent__icontains=query)
|
||||
|
||||
self.queryset = (
|
||||
self.queryset
|
||||
.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))),
|
||||
default=Value(0))) # put exact matches at the top of the result set
|
||||
.filter(name__icontains=query).order_by('-starts', 'name')
|
||||
.filter(filter).order_by('-starts', 'name')
|
||||
)
|
||||
|
||||
updated_at = self.request.query_params.get('updated_at', None)
|
||||
@@ -644,7 +648,6 @@ 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()
|
||||
@@ -717,16 +720,27 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
obj = self.get_object()
|
||||
ingredients = request.data.get('ingredients', None)
|
||||
servings = request.data.get('servings', None)
|
||||
list_recipe = ShoppingListRecipe.objects.filter(id=request.data.get('list_recipe', None)).first()
|
||||
if servings is None:
|
||||
servings = getattr(list_recipe, 'servings', obj.servings)
|
||||
# created_by needs to be sticky to original creator as it is 'their' shopping list
|
||||
# changing shopping list created_by can shift some items to new owner which may not share in the other direction
|
||||
created_by = getattr(ShoppingListEntry.objects.filter(list_recipe=list_recipe).first(), 'created_by', request.user)
|
||||
content = {'msg': _(f'{obj.name} was added to the shopping list.')}
|
||||
list_from_recipe(list_recipe=list_recipe, recipe=obj, ingredients=ingredients, servings=servings, space=request.space, created_by=created_by)
|
||||
list_recipe = request.data.get('list_recipe', None)
|
||||
mealplan = request.data.get('mealplan', None)
|
||||
SLR = RecipeShoppingEditor(request.user, request.space, id=list_recipe, recipe=obj, mealplan=mealplan)
|
||||
|
||||
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
||||
content = {'msg': _(f'{obj.name} was added to the shopping list.')}
|
||||
http_status = status.HTTP_204_NO_CONTENT
|
||||
if servings and servings <= 0:
|
||||
result = SLR.delete()
|
||||
elif list_recipe:
|
||||
result = SLR.edit(servings=servings, ingredients=ingredients)
|
||||
else:
|
||||
result = SLR.create(servings=servings, ingredients=ingredients)
|
||||
|
||||
if not result:
|
||||
content = {'msg': ('An error occurred')}
|
||||
http_status = status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
else:
|
||||
content = {'msg': _(f'{obj.name} was added to the shopping list.')}
|
||||
http_status = status.HTTP_204_NO_CONTENT
|
||||
|
||||
return Response(content, status=http_status)
|
||||
|
||||
@decorators.action(
|
||||
detail=True,
|
||||
|
||||
Reference in New Issue
Block a user