From 1be588992384ebc1be423ddbfeaf23d7d8d68641 Mon Sep 17 00:00:00 2001 From: smilerz Date: Mon, 7 Feb 2022 17:46:04 -0600 Subject: [PATCH] tests for makenow filter --- cookbook/helper/recipe_search.py | 10 +- .../0172_food_child_inherit_fields.py | 18 +++ cookbook/models.py | 57 +++++--- cookbook/serializer.py | 3 +- cookbook/signals.py | 40 ++---- cookbook/tests/api/test_api_food.py | 44 ++++-- cookbook/tests/factories/__init__.py | 9 ++ cookbook/tests/other/test_makenow_filter.py | 135 ++++++++++++++++++ .../other/test_recipe_full_text_search.py | 27 ++++ vue/src/locales/en.json | 7 +- vue/src/utils/models.js | 13 ++ 11 files changed, 296 insertions(+), 67 deletions(-) create mode 100644 cookbook/migrations/0172_food_child_inherit_fields.py create mode 100644 cookbook/tests/other/test_makenow_filter.py create mode 100644 cookbook/tests/other/test_recipe_full_text_search.py diff --git a/cookbook/helper/recipe_search.py b/cookbook/helper/recipe_search.py index fa5feca5f..ad37af6da 100644 --- a/cookbook/helper/recipe_search.py +++ b/cookbook/helper/recipe_search.py @@ -62,7 +62,7 @@ class RecipeSearch(): # TODO add created by # TODO image exists self._sort_order = self._params.get('sort_order', None) - self._internal = str2bool(self._params.get('internal', False)) + self._internal = str2bool(self._params.get('internal', None)) self._random = str2bool(self._params.get('random', False)) self._new = str2bool(self._params.get('new', False)) self._last_viewed = int(self._params.get('last_viewed', 0)) @@ -112,7 +112,7 @@ class RecipeSearch(): self.food_filters(**self._foods) self.book_filters(**self._books) self.rating_filter(rating=self._rating) - self.internal_filter() + self.internal_filter(internal=self._internal) self.step_filters(steps=self._steps) self.unit_filters(units=self._units) self._makenow_filter() @@ -335,8 +335,10 @@ class RecipeSearch(): else: self._queryset = self._queryset.filter(rating__gte=int(rating)) - def internal_filter(self): - self._queryset = self._queryset.filter(internal=True) + def internal_filter(self, internal=None): + if not internal: + return + self._queryset = self._queryset.filter(internal=internal) def book_filters(self, **kwargs): if all([kwargs[x] is None for x in kwargs]): diff --git a/cookbook/migrations/0172_food_child_inherit_fields.py b/cookbook/migrations/0172_food_child_inherit_fields.py new file mode 100644 index 000000000..70098b2b3 --- /dev/null +++ b/cookbook/migrations/0172_food_child_inherit_fields.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.11 on 2022-02-04 17:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0171_auto_20220202_1340'), + ] + + operations = [ + migrations.AddField( + model_name='food', + name='child_inherit_fields', + field=models.ManyToManyField(blank=True, related_name='child_inherit', to='cookbook.FoodInheritField'), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index 49592027e..35d4f6d62 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -97,13 +97,6 @@ class TreeModel(MP_Node): else: return f"{self.name}" - # MP_Tree move uses raw SQL to execute move, override behavior to force a save triggering post_save signal - def move(self, *args, **kwargs): - super().move(*args, **kwargs) - # treebeard bypasses ORM, need to retrieve the object again to avoid writing previous state back to disk - obj = self.__class__.objects.get(id=self.id) - obj.save() - @property def parent(self): parent = self.get_parent() @@ -488,9 +481,9 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): - # TODO when savings a food as substitute children - assume children and descednats are also substitutes for siblings # exclude fields not implemented yet - inheritable_fields = FoodInheritField.objects.exclude(field__in=['diet', 'substitute', 'substitute_children', 'substitute_siblings']) + inheritable_fields = FoodInheritField.objects.exclude(field__in=['diet', 'substitute', ]) + # TODO add inherit children_inherit, parent_inherit, Do Not Inherit # WARNING: Food inheritance relies on post_save signals, avoid using UPDATE to update Food objects unless you intend to bypass those signals if SORT_TREE_BY_NAME: @@ -505,6 +498,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): substitute = models.ManyToManyField("self", blank=True) substitute_siblings = models.BooleanField(default=False) substitute_children = models.BooleanField(default=False) + child_inherit_fields = models.ManyToManyField(FoodInheritField, blank=True, related_name='child_inherit') space = models.ForeignKey(Space, on_delete=models.CASCADE) objects = ScopedManager(space='space', _manager_class=TreeManager) @@ -518,16 +512,27 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): else: return super().delete() + # MP_Tree move uses raw SQL to execute move, override behavior to force a save triggering post_save signal + + def move(self, *args, **kwargs): + super().move(*args, **kwargs) + # treebeard bypasses ORM, need to explicity save to trigger post save signals retrieve the object again to avoid writing previous state back to disk + obj = self.__class__.objects.get(id=self.id) + if parent := obj.get_parent(): + # child should inherit what the parent defines it should inherit + obj.inherit_fields.set(list(parent.child_inherit_fields.all() or parent.inherit_fields.all())) + obj.save() + @staticmethod def reset_inheritance(space=None, food=None): # resets inherited fields to the space defaults and updates all inherited fields to root object values if food: - inherit = list(food.inherit_fields.all().values('id', 'field')) - filter = Q(id=food.id, space=space) - tree_filter = Q(path__startswith=food.path, space=space) + # if child inherit fields is preset children should be set to that, otherwise inherit this foods inherited fields + inherit = list((food.child_inherit_fields.all() or food.inherit_fields.all()).values('id', 'field')) + tree_filter = Q(path__startswith=food.path, space=space, depth=food.depth+1) else: inherit = list(space.food_inherit.all().values('id', 'field')) - filter = tree_filter = Q(space=space) + tree_filter = Q(space=space) # remove all inherited fields from food Through = Food.objects.filter(tree_filter).first().inherit_fields.through @@ -542,16 +547,26 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): ]) inherit = [x['field'] for x in inherit] - if 'ignore_shopping' in inherit: - # get food at root that have children that need updated - Food.include_descendants(queryset=Food.objects.filter(Q(depth=1, numchild__gt=0, ignore_shopping=True) & filter)).update(ignore_shopping=True) - Food.include_descendants(queryset=Food.objects.filter(Q(depth=1, numchild__gt=0, ignore_shopping=False) & filter)).update(ignore_shopping=False) + for field in ['ignore_shopping', 'substitute_children', 'substitute_siblings']: + if field in inherit: + if food and getattr(food, field, None): + food.get_descendants().update(**{f"{field}": True}) + elif food and not getattr(food, field, True): + food.get_descendants().update(**{f"{field}": False}) + else: + # get food at root that have children that need updated + Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, **{f"{field}": True}, space=space)).update(**{f"{field}": True}) + Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, **{f"{field}": False}, space=space)).update(**{f"{field}": 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 - category_roots = Food.exclude_descendants(queryset=Food.objects.filter(Q(supermarket_category__isnull=False, numchild__gt=0) & filter)) - for root in category_roots: - root.get_descendants().update(supermarket_category=root.supermarket_category) + if food and food.supermarket_category: + food.get_descendants().update(supermarket_category=food.supermarket_category) + elif food is None: + # find top node that has category set + category_roots = Food.exclude_descendants(queryset=Food.objects.filter(supermarket_category__isnull=False, numchild__gt=0, space=space)) + for root in category_roots: + root.get_descendants().update(supermarket_category=root.supermarket_category) class Meta: constraints = [ diff --git a/cookbook/serializer.py b/cookbook/serializer.py index ec6567507..cd553741c 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -389,6 +389,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR # shopping = serializers.SerializerMethodField('get_shopping_status') shopping = serializers.ReadOnlyField(source='shopping_status') inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False) + child_inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False) food_onhand = CustomOnHandField(required=False, allow_null=True) substitute_onhand = serializers.SerializerMethodField('get_substitute_onhand') substitute = FoodSimpleSerializer(many=True, allow_null=True, required=False) @@ -461,7 +462,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR fields = ( 'id', 'name', 'description', 'shopping', 'recipe', 'food_onhand', 'supermarket_category', 'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping', - 'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand' + 'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields' ) read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe') diff --git a/cookbook/signals.py b/cookbook/signals.py index 17a3da863..f405a5218 100644 --- a/cookbook/signals.py +++ b/cookbook/signals.py @@ -18,9 +18,8 @@ if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_ps 'django.db.backends.postgresql']: SQLITE = False + # wraps a signal with the ability to set 'skip_signal' to avoid creating recursive signals - - def skip_signal(signal_func): @wraps(signal_func) def _decorator(sender, instance, **kwargs): @@ -76,8 +75,9 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs): # apply changes from parent to instance for each inherited field if instance.parent and inherit.count() > 0: parent = instance.get_parent() - if 'ignore_shopping' in inherit: - instance.ignore_shopping = parent.ignore_shopping + for field in ['ignore_shopping', 'substitute_children', 'substitute_siblings']: + if field in inherit: + setattr(instance, field, getattr(parent, field, None)) # 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 @@ -87,19 +87,17 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs): finally: del instance.skip_signal - # 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='ignore_shopping'): - child.ignore_shopping = instance.ignore_shopping - _save.append(child) - # don't cascade empty supermarket category - if instance.supermarket_category: - # apply changes to direct children - depend on save signals for those objects to cascade inheritance down - for child in instance.get_children().filter(inherit_fields__field='supermarket_category'): - child.supermarket_category = instance.supermarket_category - _save.append(child) - for child in set(_save): + for child in instance.get_children().filter(inherit_fields__in=Food.inheritable_fields): + # set inherited field values + for field in (inherit_fields := ['ignore_shopping', 'substitute_children', 'substitute_siblings']): + if field in instance.inherit_fields.values_list('field', flat=True): + setattr(child, field, getattr(instance, field, None)) + + # don't cascade empty supermarket category + if instance.supermarket_category and 'supermarket_category' in inherit_fields: + setattr(child, 'supermarket_category', getattr(instance, 'supermarket_category', None)) + child.save() @@ -117,19 +115,9 @@ def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs 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 created: - # if creating a mealplan - perform shopping list activities - # 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) diff --git a/cookbook/tests/api/test_api_food.py b/cookbook/tests/api/test_api_food.py index 699ededa9..9049a7906 100644 --- a/cookbook/tests/api/test_api_food.py +++ b/cookbook/tests/api/test_api_food.py @@ -485,6 +485,10 @@ def test_tree_filter(obj_tree_1, obj_2, obj_3, u1_s1): ({'has_category': True, 'inherit': False}, 'supermarket_category', False, 'cat_1'), ({'ignore_shopping': True, 'inherit': True}, 'ignore_shopping', True, 'false'), ({'ignore_shopping': True, 'inherit': False}, 'ignore_shopping', False, 'false'), + ({'substitute_children': True, 'inherit': True}, 'substitute_children', True, 'false'), + ({'substitute_children': True, 'inherit': False}, 'substitute_children', False, 'false'), + ({'substitute_siblings': True, 'inherit': True}, 'substitute_siblings', True, 'false'), + ({'substitute_siblings': True, 'inherit': False}, 'substitute_siblings', 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): @@ -507,28 +511,42 @@ def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1): assert (getattr(obj_tree_1, field) == new_val) == inherit assert (getattr(child, field) == new_val) == inherit +# TODO add test_inherit with child_inherit + @pytest.mark.parametrize("obj_tree_1", [ - ({'has_category': True, 'inherit': False, 'ignore_shopping': True}), + ({'has_category': True, 'inherit': False, 'ignore_shopping': True, 'substitute_children': True, 'substitute_siblings': True}), ], indirect=['obj_tree_1']) -def test_reset_inherit(obj_tree_1, space_1): +@pytest.mark.parametrize("global_reset", [True, False]) +@pytest.mark.parametrize("field", ['ignore_shopping', 'substitute_children', 'substitute_siblings', 'supermarket_category']) +def test_reset_inherit_space_fields(obj_tree_1, space_1, global_reset, field): 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.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 - parent.reset_inheritance(space=space_1) + if field == 'supermarket_category': + assert parent.supermarket_category != child.supermarket_category + assert parent.supermarket_category != obj_tree_1.supermarket_category + else: + setattr(obj_tree_1, field, False) + obj_tree_1.save() + assert getattr(parent, field) == getattr(child, field) + assert getattr(parent, field) != getattr(obj_tree_1, field) + + if global_reset: + space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True)) # set default inherit fields + parent.reset_inheritance(space=space_1) + else: + obj_tree_1.child_inherit_fields.set(Food.inheritable_fields.values_list('id', flat=True)) + obj_tree_1.save() + parent.reset_inheritance(space=space_1, food=obj_tree_1) # djangotree bypasses ORM and need to be retrieved again 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.ignore_shopping == obj_tree_1.ignore_shopping == child.ignore_shopping - assert parent.supermarket_category == obj_tree_1.supermarket_category == child.supermarket_category + parent = Food.objects.get(id=parent.id) + child = Food.objects.get(id=child.id) + + assert (getattr(parent, field) == getattr(obj_tree_1, field)) == global_reset + assert getattr(obj_tree_1, field) == getattr(child, field) def test_onhand(obj_1, u1_s1, u2_s1): diff --git a/cookbook/tests/factories/__init__.py b/cookbook/tests/factories/__init__.py index 070b2b70c..4a87081f0 100644 --- a/cookbook/tests/factories/__init__.py +++ b/cookbook/tests/factories/__init__.py @@ -111,6 +111,15 @@ class FoodFactory(factory.django.DjangoModelFactory): ) space = factory.SubFactory(SpaceFactory) + @factory.post_generation + def users_onhand(self, create, extracted, **kwargs): + if not create: + return + + if extracted: + for user in extracted: + self.onhand_users.add(user) + class Params: has_category = False has_recipe = False diff --git a/cookbook/tests/other/test_makenow_filter.py b/cookbook/tests/other/test_makenow_filter.py new file mode 100644 index 000000000..e19ed0fa4 --- /dev/null +++ b/cookbook/tests/other/test_makenow_filter.py @@ -0,0 +1,135 @@ +import json + +import pytest +from django.contrib import auth +from django.urls import reverse +from django_scopes import scope +from pytest_factoryboy import LazyFixture, register + +from cookbook.helper.recipe_search import RecipeSearch +from cookbook.models import Food, Recipe +from cookbook.tests.factories import FoodFactory, RecipeFactory + +# TODO returns recipes with all ingredients via child substitute +# TODO returns recipes with all ingredients via sibling substitute + + +@pytest.fixture +def recipes(space_1): + return RecipeFactory.create_batch(10, space=space_1) + + +@pytest.fixture +def makenow_recipe(request, space_1): + onhand_user = auth.get_user(request.getfixturevalue(request.param.get('onhand_users', 'u1_s1'))) + + recipe = RecipeFactory.create(space=space_1) + for food in Food.objects.filter(ingredient__step__recipe=recipe.id): + food.onhand_users.add(onhand_user) + return recipe + + +@pytest.fixture +def user1(u1_s1, u2_s1, space_1): + user1 = auth.get_user(u1_s1) + user2 = auth.get_user(u2_s1) + user1.userpreference.shopping_share.add(user2) + user2.userpreference.shopping_share.add(user1) + return user1 + + +@pytest.mark.parametrize("makenow_recipe", [ + ({'onhand_users': 'u1_s1'}), ({'onhand_users': 'u2_s1'}), +], indirect=['makenow_recipe']) +def test_makenow_onhand(recipes, makenow_recipe, user1, space_1): + request = type('', (object,), {'space': space_1, 'user': user1})() + search = RecipeSearch(request, makenow='true') + with scope(space=space_1): + search = search.get_queryset(Recipe.objects.all()) + assert search.count() == 1 + assert search.first().id == makenow_recipe.id + + +@pytest.mark.parametrize("makenow_recipe", [ + ({'onhand_users': 'u1_s1'}), ({'onhand_users': 'u2_s1'}), +], indirect=['makenow_recipe']) +def test_makenow_ignoreshopping(recipes, makenow_recipe, user1, space_1): + request = type('', (object,), {'space': space_1, 'user': user1})() + search = RecipeSearch(request, makenow='true') + with scope(space=space_1): + food = Food.objects.filter(ingredient__step__recipe=makenow_recipe.id).first() + food.onhand_users.clear() + assert search.get_queryset(Recipe.objects.all()) == 0 + food.ignore_shopping = True + food.save() + assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9 + assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, ignore_shopping=True).count() == 1 + search = search.get_queryset(Recipe.objects.all()) + assert search.count() == 1 + assert search.first().id == makenow_recipe.id + + +@pytest.mark.parametrize("makenow_recipe", [ + ({'onhand_users': 'u1_s1'}), ({'onhand_users': 'u2_s1'}), +], indirect=['makenow_recipe']) +def test_makenow_substitute(recipes, makenow_recipe, user1, space_1): + request = type('', (object,), {'space': space_1, 'user': user1})() + search = RecipeSearch(request, makenow='true') + with scope(space=space_1): + food = Food.objects.filter(ingredient__step__recipe=makenow_recipe.id).first() + onhand_user = food.onhand_users.first() + food.onhand_users.clear() + assert search.get_queryset(Recipe.objects.all()).count() == 0 + food.substitute.add(FoodFactory.create(space=space_1, onhand_users=[onhand_user])) + assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9 + assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, substitute__isnull=False).count() == 1 + + search = search.get_queryset(Recipe.objects.all()) + assert search.count() == 1 + assert search.first().id == makenow_recipe.id + + +@pytest.mark.parametrize("makenow_recipe", [ + ({'onhand_users': 'u1_s1'}), ({'onhand_users': 'u2_s1'}), +], indirect=['makenow_recipe']) +def test_makenow_child_substitute(recipes, makenow_recipe, user1, space_1): + request = type('', (object,), {'space': space_1, 'user': user1})() + search = RecipeSearch(request, makenow='true') + with scope(space=space_1): + food = Food.objects.filter(ingredient__step__recipe=makenow_recipe.id).first() + onhand_user = food.onhand_users.first() + food.onhand_users.clear() + food.substitute_children = True + food.save() + assert search.get_queryset(Recipe.objects.all()).count() == 0 + new_food = FoodFactory.create(space=space_1, onhand_users=[onhand_user]) + new_food.move(food, 'first-child') + assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9 + assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, numchild__gt=0).count() == 1 + search = search.get_queryset(Recipe.objects.all()) + assert search.count() == 1 + assert search.first().id == makenow_recipe.id + + +@pytest.mark.parametrize("makenow_recipe", [ + ({'onhand_users': 'u1_s1'}), ({'onhand_users': 'u2_s1'}), +], indirect=['makenow_recipe']) +def test_makenow_sibling_substitute(recipes, makenow_recipe, user1, space_1): + request = type('', (object,), {'space': space_1, 'user': user1})() + search = RecipeSearch(request, makenow='true') + with scope(space=space_1): + food = Food.objects.filter(ingredient__step__recipe=makenow_recipe.id).first() + onhand_user = food.onhand_users.first() + food.onhand_users.clear() + food.substitute_siblings = True + food.save() + assert search.get_queryset(Recipe.objects.all()).count() == 0 + new_parent = FoodFactory.create(space=space_1) + new_sibling = FoodFactory.create(space=space_1, onhand_users=[onhand_user]) + new_sibling.move(new_parent, 'first-child') + food.move(new_parent, 'first-child') + assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9 + assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, depth=2).count() == 1 + search = search.get_queryset(Recipe.objects.all()) + assert search.count() == 1 + assert search.first().id == makenow_recipe.id diff --git a/cookbook/tests/other/test_recipe_full_text_search.py b/cookbook/tests/other/test_recipe_full_text_search.py new file mode 100644 index 000000000..7504f6dbb --- /dev/null +++ b/cookbook/tests/other/test_recipe_full_text_search.py @@ -0,0 +1,27 @@ +import json + +import pytest +from django.contrib import auth +from django.urls import reverse +from django_scopes import scope, scopes_disabled +from pytest_factoryboy import LazyFixture, register + +from cookbook.models import Food, FoodInheritField, Ingredient, ShoppingList, ShoppingListEntry +from cookbook.tests.factories import (FoodFactory, IngredientFactory, ShoppingListEntryFactory, + SupermarketCategoryFactory) + +# TODO food/keyword/book test or, and, or_not, and_not search +# TODO recipe name/description/instructions/keyword/book/food test search with icontains, istarts with/ full text(?? probably when word changes based on conjugation??), trigram, unaccent + +# TODO fuzzy lookup on units, keywords, food when not configured in main search settings + +# TODO test combining any/all of the above +# TODO search rating as user or when another user rated +# TODO search last cooked +# TODO changing lsat_viewed ## to return on search +# TODO test sort_by +# TODO test sort_by new X number of recipes are new within last Y days +# TODO test loading custom filter +# TODO test loading custom filter with overrided params +# TODO makenow with above filters +# TODO test search for number of times cooked (self vs others) diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index a10ffc40f..0d2802264 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -328,11 +328,14 @@ "view_recipe": "View Recipe", "filter": "Filter", "reset_children": "Reset Child Inheritance", - "reset_children_help": "Overwrite all children with values from inherited fields.", + "reset_children_help": "Overwrite all children with values from inherited fields. Inheritted fields of children will be set to Inherit Fields unless Children Inherit Fields is set.", "substitute_help": "Substitutes are considered when searching for recipes that can be made with onhand ingredients.", "substitute_siblings_help": "All food that share a parent of this food are considered substitutes.", "substitute_children_help": "All food that are children of this food are considered substitutes.", "substitute_siblings": "Substitute Siblings", "substitute_children": "Substitute Children", - "SubstituteOnHand": "You have a substitute on hand." + "SubstituteOnHand": "You have a substitute on hand.", + "ChildInheritFields": "Children Inherit Fields", + "ChildInheritFields_help": "Children will inherit these fields by default.", + "InheritFields_help": "The values of these fields will be inheritted from parent (Exception: blank shopping categories are not inheritted)" } diff --git a/vue/src/utils/models.js b/vue/src/utils/models.js index b6a373da2..f283b9689 100644 --- a/vue/src/utils/models.js +++ b/vue/src/utils/models.js @@ -90,6 +90,7 @@ export class Models { "substitute_siblings", "substitute_children", "reset_inherit", + "child_inherit_fields", ], ], @@ -179,6 +180,18 @@ export class Models { list: "FOOD_INHERIT_FIELDS", label: i18n.t("InheritFields"), condition: { field: "food_children_exist", value: true, condition: "preference_equals" }, + help_text: i18n.t("InheritFields_help"), + }, + child_inherit_fields: { + form_field: true, + advanced: true, + type: "lookup", + multiple: true, + field: "child_inherit_fields", + list: "FOOD_INHERIT_FIELDS", + label: i18n.t("ChildInheritFields"), + condition: { field: "numchild", value: 0, condition: "gt" }, + help_text: i18n.t("ChildInheritFields_help"), }, reset_inherit: { form_field: true,