diff --git a/cookbook/apps.py b/cookbook/apps.py index c785e5536..e551319db 100644 --- a/cookbook/apps.py +++ b/cookbook/apps.py @@ -35,4 +35,3 @@ class CookbookConfig(AppConfig): # if DEBUG: # traceback.print_exc() # pass # dont break startup just because fix could not run, need to investigate cases when this happens - diff --git a/cookbook/forms.py b/cookbook/forms.py index ea86d1cb8..8cd5d68f8 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -481,7 +481,7 @@ class ShoppingPreferenceForm(forms.ModelForm): fields = ( 'shopping_share', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'mealplan_autoexclude_onhand', - 'mealplan_autoinclude_related', 'default_delay', 'filter_to_supermarket', 'shopping_recent_days', 'csv_delim', 'csv_prefix' + 'mealplan_autoinclude_related', 'shopping_add_onhand', 'default_delay', 'filter_to_supermarket', 'shopping_recent_days', 'csv_delim', 'csv_prefix' ) help_texts = { @@ -496,6 +496,7 @@ class ShoppingPreferenceForm(forms.ModelForm): 'default_delay': _('Default number of hours to delay a shopping list entry.'), 'filter_to_supermarket': _('Filter shopping list to only include supermarket categories.'), 'shopping_recent_days': _('Days of recent shopping list entries to display.'), + 'shopping_add_onhand': _("Mark food 'On Hand' when checked off shopping list."), 'csv_delim': _('Delimiter to use for CSV exports.'), 'csv_prefix': _('Prefix to add when copying list to the clipboard.'), @@ -510,7 +511,8 @@ class ShoppingPreferenceForm(forms.ModelForm): 'filter_to_supermarket': _('Filter to Supermarket'), 'shopping_recent_days': _('Recent Days'), 'csv_delim': _('CSV Delimiter'), - "csv_prefix_label": _("List Prefix") + "csv_prefix_label": _("List Prefix"), + 'shopping_add_onhand': _("Auto On Hand"), } widgets = { diff --git a/cookbook/helper/scope_middleware.py b/cookbook/helper/scope_middleware.py index 7f0676c7e..714214ac2 100644 --- a/cookbook/helper/scope_middleware.py +++ b/cookbook/helper/scope_middleware.py @@ -5,6 +5,7 @@ from rest_framework.authtoken.models import Token from rest_framework.exceptions import AuthenticationFailed from cookbook.views import views +from recipes import settings class ScopeMiddleware: @@ -14,14 +15,15 @@ class ScopeMiddleware: def __call__(self, request): if request.user.is_authenticated: - if request.path.startswith('/admin/'): + prefix = settings.JS_REVERSE_SCRIPT_PREFIX or '' + if request.path.startswith(prefix + '/admin/'): with scopes_disabled(): return self.get_response(request) - if request.path.startswith('/signup/') or request.path.startswith('/invite/'): + if request.path.startswith(prefix + '/signup/') or request.path.startswith(prefix + '/invite/'): return self.get_response(request) - if request.path.startswith('/accounts/'): + if request.path.startswith(prefix + '/accounts/'): return self.get_response(request) with scopes_disabled(): @@ -36,7 +38,7 @@ class ScopeMiddleware: with scope(space=request.space): return self.get_response(request) else: - if request.path.startswith('/api/'): + if request.path.startswith(prefix + '/api/'): try: if auth := TokenAuthentication().authenticate(request): request.space = auth[0].userpreference.space diff --git a/cookbook/helper/shopping_helper.py b/cookbook/helper/shopping_helper.py index 75a70d36b..658f63caa 100644 --- a/cookbook/helper/shopping_helper.py +++ b/cookbook/helper/shopping_helper.py @@ -17,8 +17,7 @@ def shopping_helper(qs, request): supermarket = request.query_params.get('supermarket', None) checked = request.query_params.get('checked', 'recent') user = request.user - - supermarket_order = ['food__supermarket_category__name', 'food__name'] + supermarket_order = [F('food__supermarket_category__name').asc(nulls_first=True), 'food__name'] # TODO created either scheduled task or startup task to delete very old shopping list entries # TODO create user preference to define 'very old' @@ -82,7 +81,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None ingredients = Ingredient.objects.filter(step__recipe=r, space=space) if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand: - ingredients = ingredients.exclude(food__food_onhand=True) + 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 @@ -93,7 +92,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None # 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, food__food_onhand=False, space=space).values_list('id', flat=True) + 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) @@ -101,7 +100,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None 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__food_onhand=False, space=space) + x_ing = Ingredient.objects.filter(step__recipe=x, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users]) else: x_ing = Ingredient.objects.filter(step__recipe=x, space=space) for i in [x for x in x_ing]: diff --git a/cookbook/migrations/0163_auto_20220105_0758.py b/cookbook/migrations/0163_auto_20220105_0758.py new file mode 100644 index 000000000..b7d67cd18 --- /dev/null +++ b/cookbook/migrations/0163_auto_20220105_0758.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.10 on 2022-01-05 13:58 + +from django.conf import settings +from django.db import migrations, models + +from cookbook.models import FoodInheritField + + +def rename_inherit_field(apps, schema_editor): + x = FoodInheritField.objects.filter(name='On Hand', field='food_onhand').first() + if x: + x.name = "Ignore Shopping" + x.field = "ignore_shopping" + x.save() + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('cookbook', '0162_userpreference_csv_delim'), + ] + + operations = [ + migrations.AddField( + model_name='food', + name='onhand_users', + field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='userpreference', + name='shopping_add_onhand', + field=models.BooleanField(default=True), + ), + migrations.RenameField( + model_name='food', + old_name='food_onhand', + new_name='ignore_shopping', + ), + migrations.RunPython(rename_inherit_field), + ] diff --git a/cookbook/models.py b/cookbook/models.py index bc167ba47..cfe4ab814 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -332,6 +332,7 @@ class UserPreference(models.Model, PermissionModelMixin): mealplan_autoadd_shopping = models.BooleanField(default=False) mealplan_autoexclude_onhand = models.BooleanField(default=True) mealplan_autoinclude_related = models.BooleanField(default=True) + shopping_add_onhand = models.BooleanField(default=True) filter_to_supermarket = models.BooleanField(default=False) default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4) shopping_recent_days = models.PositiveIntegerField(default=7) @@ -489,10 +490,11 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): node_order_by = ['name'] name = models.CharField(max_length=128, validators=[MinLengthValidator(1)]) recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL) - supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL) - food_onhand = models.BooleanField(default=False) # inherited field + supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL) # inherited field + ignore_shopping = models.BooleanField(default=False) # inherited field + onhand_users = models.ManyToManyField(User, blank=True) description = models.TextField(default='', blank=True) - inherit_fields = models.ManyToManyField(FoodInheritField, blank=True) # inherited field: is this name better as inherit instead of ignore inherit? which is more intuitive? + inherit_fields = models.ManyToManyField(FoodInheritField, blank=True) space = models.ForeignKey(Space, on_delete=models.CASCADE) objects = ScopedManager(space='space', _manager_class=TreeManager) @@ -524,10 +526,10 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): ]) inherit = inherit.values_list('field', flat=True) - if 'food_onhand' in inherit: + if 'ignore_shopping' in inherit: # get food at root that have children that need updated - Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, food_onhand=True)).update(food_onhand=True) - Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, food_onhand=False)).update(food_onhand=False) + Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, ignore_shopping=True)).update(ignore_shopping=True) + Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, ignore_shopping=False)).update(ignore_shopping=False) if 'supermarket_category' in inherit: # when supermarket_category is null or blank assuming it is not set and not intended to be blank for all descedants # find top node that has category set diff --git a/cookbook/serializer.py b/cookbook/serializer.py index c43b52a7c..ac6238681 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -10,6 +10,7 @@ from django.utils import timezone from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer from rest_framework import serializers from rest_framework.exceptions import NotFound, ValidationError +from rest_framework.fields import empty from cookbook.helper.shopping_helper import list_from_recipe from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Food, @@ -92,6 +93,18 @@ class CustomDecimalField(serializers.Field): raise ValidationError('A valid number is required') +class CustomOnHandField(serializers.Field): + def get_attribute(self, instance): + return instance + + def to_representation(self, obj): + shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id] + return obj.onhand_users.filter(id__in=shared_users).exists() + + def to_internal_value(self, data): + return data + + class SpaceFilterSerializer(serializers.ListSerializer): def to_representation(self, data): @@ -167,16 +180,13 @@ class UserPreferenceSerializer(serializers.ModelSerializer): raise NotFound() return super().create(validated_data) - def update(self, instance, validated_data): - # don't allow writing to FoodInheritField via API - return super().update(instance, validated_data) - class Meta: model = UserPreference fields = ( 'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_kj', 'search_style', 'show_recent', 'plan_share', 'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_inherit_default', 'default_delay', - 'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', 'csv_delim', 'csv_prefix', 'filter_to_supermarket' + 'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', 'csv_delim', 'csv_prefix', + 'filter_to_supermarket', 'shopping_add_onhand' ) @@ -371,6 +381,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR recipe = RecipeSimpleSerializer(allow_null=True, required=False) shopping = serializers.SerializerMethodField('get_shopping_status') inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False) + food_onhand = CustomOnHandField(required=False, allow_null=True) recipe_filter = 'steps__ingredients__food' @@ -385,12 +396,29 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR validated_data['supermarket_category'], sc_created = SupermarketCategory.objects.get_or_create( name=validated_data.pop('supermarket_category')['name'], space=self.context['request'].space) + onhand = validated_data.get('food_onhand', None) + + # assuming if on hand for user also onhand for shopping_share users + if not onhand is None: + shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all()) + if onhand: + validated_data['onhand_users'] = list(self.instance.onhand_users.all()) + shared_users + else: + validated_data['onhand_users'] = list(set(self.instance.onhand_users.all()) - set(shared_users)) obj, created = Food.objects.get_or_create(**validated_data) return obj def update(self, instance, validated_data): if name := validated_data.get('name', None): validated_data['name'] = name.strip() + # assuming if on hand for user also onhand for shopping_share users + onhand = validated_data.get('food_onhand', None) + if not onhand is None: + shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all()) + if onhand: + validated_data['onhand_users'] = list(self.instance.onhand_users.all()) + shared_users + else: + validated_data['onhand_users'] = list(set(self.instance.onhand_users.all()) - set(shared_users)) return super(FoodSerializer, self).update(instance, validated_data) class Meta: @@ -687,20 +715,22 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer): return fields def run_validation(self, data): - if ( - data.get('checked', False) - and self.root.instance - and not self.root.instance.checked - ): - # if checked flips from false to true set completed datetime - data['completed_at'] = timezone.now() - elif not data.get('checked', False): - # if not checked set completed to None - data['completed_at'] = None - else: - # otherwise don't write anything - if 'completed_at' in data: - del data['completed_at'] + if self.root.instance.__class__.__name__ == 'ShoppingListEntry': + if ( + data.get('checked', False) + and self.root.instance + and not self.root.instance.checked + ): + # if checked flips from false to true set completed datetime + data['completed_at'] = timezone.now() + + elif not data.get('checked', False): + # if not checked set completed to None + data['completed_at'] = None + else: + # otherwise don't write anything + if 'completed_at' in data: + del data['completed_at'] return super().run_validation(data) @@ -709,6 +739,16 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer): validated_data['created_by'] = self.context['request'].user return super().create(validated_data) + def update(self, instance, validated_data): + user = self.context['request'].user + # update the onhand for food if shopping_add_onhand is True + if user.userpreference.shopping_add_onhand: + if checked := validated_data.get('checked', None): + instance.food.onhand_users.add(*user.userpreference.shopping_share.all(), user) + elif checked == False: + instance.food.onhand_users.remove(*user.userpreference.shopping_share.all(), user) + return super().update(instance, validated_data) + class Meta: model = ShoppingListEntry fields = ( @@ -861,7 +901,7 @@ class FoodExportSerializer(FoodSerializer): class Meta: model = Food - fields = ('name', 'food_onhand', 'supermarket_category',) + fields = ('name', 'ignore_shopping', 'supermarket_category',) class IngredientExportSerializer(WritableNestedModelSerializer): diff --git a/cookbook/signals.py b/cookbook/signals.py index 47e5bf9ea..c21d66389 100644 --- a/cookbook/signals.py +++ b/cookbook/signals.py @@ -75,8 +75,8 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs): # apply changes from parent to instance for each inheritted field if instance.parent and inherit.count() > 0: parent = instance.get_parent() - if 'food_onhand' in inherit: - instance.food_onhand = parent.food_onhand + if 'ignore_shopping' in inherit: + instance.ignore_shopping = parent.ignore_shopping # if supermarket_category is not set, do not cascade - if this becomes non-intuitive can change if 'supermarket_category' in inherit and parent.supermarket_category: instance.supermarket_category = parent.supermarket_category @@ -89,8 +89,8 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs): # TODO figure out how to generalize this # apply changes to direct children - depend on save signals for those objects to cascade inheritance down _save = [] - for child in instance.get_children().filter(inherit_fields__field='food_onhand'): - child.food_onhand = instance.food_onhand + for child in instance.get_children().filter(inherit_fields__field='ignore_shopping'): + child.ignore_shopping = instance.ignore_shopping _save.append(child) # don't cascade empty supermarket category if instance.supermarket_category: @@ -121,3 +121,11 @@ def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs 'servings': instance.servings } list_recipe = list_from_recipe(**kwargs) + + +# user = self.context['request'].user +# if user.userpreference.shopping_add_onhand: +# if checked := validated_data.get('checked', None): +# instance.food.onhand_users.add(*user.userpreference.shopping_share.all(), user) +# elif checked == False: +# instance.food.onhand_users.remove(*user.userpreference.shopping_share.all(), user) diff --git a/cookbook/tests/api/test_api_food.py b/cookbook/tests/api/test_api_food.py index cde725484..699ededa9 100644 --- a/cookbook/tests/api/test_api_food.py +++ b/cookbook/tests/api/test_api_food.py @@ -483,8 +483,8 @@ def test_tree_filter(obj_tree_1, obj_2, obj_3, u1_s1): @pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [ ({'has_category': True, 'inherit': True}, 'supermarket_category', True, 'cat_1'), ({'has_category': True, 'inherit': False}, 'supermarket_category', False, 'cat_1'), - ({'food_onhand': True, 'inherit': True}, 'food_onhand', True, 'false'), - ({'food_onhand': True, 'inherit': False}, 'food_onhand', False, 'false'), + ({'ignore_shopping': True, 'inherit': True}, 'ignore_shopping', True, 'false'), + ({'ignore_shopping': True, 'inherit': False}, 'ignore_shopping', False, 'false'), ], indirect=['obj_tree_1']) # indirect=True populates magic variable request.param of obj_tree_1 with the parameter def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1): with scope(space=obj_tree_1.space): @@ -509,16 +509,16 @@ def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1): @pytest.mark.parametrize("obj_tree_1", [ - ({'has_category': True, 'inherit': False, 'food_onhand': True}), + ({'has_category': True, 'inherit': False, 'ignore_shopping': True}), ], indirect=['obj_tree_1']) def test_reset_inherit(obj_tree_1, space_1): with scope(space=space_1): space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True)) # set default inherit fields parent = obj_tree_1.get_parent() child = obj_tree_1.get_descendants()[0] - obj_tree_1.food_onhand = False - assert parent.food_onhand == child.food_onhand - assert parent.food_onhand != obj_tree_1.food_onhand + obj_tree_1.ignore_shopping = False + assert parent.ignore_shopping == child.ignore_shopping + assert parent.ignore_shopping != obj_tree_1.ignore_shopping assert parent.supermarket_category != child.supermarket_category assert parent.supermarket_category != obj_tree_1.supermarket_category @@ -527,5 +527,26 @@ def test_reset_inherit(obj_tree_1, space_1): obj_tree_1 = Food.objects.get(id=obj_tree_1.id) parent = obj_tree_1.get_parent() child = obj_tree_1.get_descendants()[0] - assert parent.food_onhand == obj_tree_1.food_onhand == child.food_onhand + assert parent.ignore_shopping == obj_tree_1.ignore_shopping == child.ignore_shopping assert parent.supermarket_category == obj_tree_1.supermarket_category == child.supermarket_category + + +def test_onhand(obj_1, u1_s1, u2_s1): + assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False + assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False + + u1_s1.patch( + reverse( + DETAIL_URL, + args={obj_1.id} + ), + {'food_onhand': True}, + content_type='application/json' + ) + assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == True + assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False + + user1 = auth.get_user(u1_s1) + user2 = auth.get_user(u2_s1) + user1.userpreference.shopping_share.add(user2) + assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == True diff --git a/cookbook/tests/api/test_api_related_recipe.py b/cookbook/tests/api/test_api_related_recipe.py index ce6221880..1a381ed5c 100644 --- a/cookbook/tests/api/test_api_related_recipe.py +++ b/cookbook/tests/api/test_api_related_recipe.py @@ -65,7 +65,6 @@ def test_related_mixed_space(request, recipe, u1_s2): reverse(RELATED_URL, args={recipe.id})).content)) == 0 -# TODO add tests for mealplan related when thats added # TODO if/when related recipes includes multiple levels (related recipes of related recipes) add the following tests # -- step recipes included in step recipes # -- step recipes included in food recipes diff --git a/cookbook/tests/api/test_api_shopping_list_entryv2.py b/cookbook/tests/api/test_api_shopping_list_entryv2.py index 40b64a726..c5a624222 100644 --- a/cookbook/tests/api/test_api_shopping_list_entryv2.py +++ b/cookbook/tests/api/test_api_shopping_list_entryv2.py @@ -217,3 +217,6 @@ def test_recent(sle, u1_s1): r = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?recent=1').content) assert len(r) == 10 assert [x['checked'] for x in r].count(False) == 9 + + +# TODO test auto onhand diff --git a/cookbook/tests/api/test_api_shopping_recipe.py b/cookbook/tests/api/test_api_shopping_recipe.py index 438de046e..8f4185cb8 100644 --- a/cookbook/tests/api/test_api_shopping_recipe.py +++ b/cookbook/tests/api/test_api_shopping_recipe.py @@ -198,11 +198,11 @@ def test_shopping_recipe_userpreference(recipe, sle_count, use_mealplan, user2): # setup recipe with 10 ingredients, 1 step recipe with 10 ingredients, 2 food onhand(from recipe and step_recipe) ingredients = Ingredient.objects.filter(step__recipe=recipe) food = Food.objects.get(id=ingredients[2].food.id) - food.food_onhand = True + food.onhand_users.add(user) food.save() food = recipe.steps.filter(type=Step.RECIPE).first().step_recipe.steps.first().ingredients.first().food food = Food.objects.get(id=food.id) - food.food_onhand = True + food.onhand_users.add(user) food.save() if use_mealplan: @@ -233,7 +233,6 @@ def test_shopping_recipe_mixed_authors(u1_s1, u2_s1): assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0 -# TODO test adding recipe with ingredients that are not food @pytest.mark.parametrize("recipe", [{'steps__ingredients__header': 1}], indirect=['recipe']) def test_shopping_with_header_ingredient(u1_s1, recipe): # with scope(space=recipe.space): diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 69f19255d..058216c0d 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -689,8 +689,11 @@ class RecipeViewSet(viewsets.ModelViewSet): obj = self.get_object() if obj.get_space() != request.space: raise PermissionDenied(detail='You do not have the required permission to perform this action', code=403) - qs = obj.get_related_recipes(levels=1) # TODO: make levels a user setting, included in request data?, keep solely in the backend? - # mealplans= TODO get todays mealplans + try: + levels = int(request.query_params.get('levels', 1)) + except (ValueError, TypeError): + levels = 1 + qs = obj.get_related_recipes(levels=levels) # TODO: make levels a user setting, included in request data?, keep solely in the backend? return Response(self.serializer_class(qs, many=True).data) diff --git a/cookbook/views/views.py b/cookbook/views/views.py index b875bc389..afc49a7a4 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -388,6 +388,7 @@ def user_settings(request): up.filter_to_supermarket = shopping_form.cleaned_data['filter_to_supermarket'] up.default_delay = shopping_form.cleaned_data['default_delay'] up.shopping_recent_days = shopping_form.cleaned_data['shopping_recent_days'] + up.shopping_add_onhand = shopping_form.cleaned_data['shopping_add_onhand'] up.csv_delim = shopping_form.cleaned_data['csv_delim'] up.csv_prefix = shopping_form.cleaned_data['csv_prefix'] if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL: diff --git a/vue/src/apps/ModelListView/ModelListView.vue b/vue/src/apps/ModelListView/ModelListView.vue index d889267c7..1d90667de 100644 --- a/vue/src/apps/ModelListView/ModelListView.vue +++ b/vue/src/apps/ModelListView/ModelListView.vue @@ -7,7 +7,7 @@