From 79b4bc387ed4fca59a3f9614cd3d2b60354f44bc Mon Sep 17 00:00:00 2001 From: smilerz Date: Thu, 30 Dec 2021 12:54:39 -0600 Subject: [PATCH] change ingore_inherit to inherit_fields --- cookbook/forms.py | 4 +- .../0159_add_shoppinglistentry_fields.py | 17 +-- .../migrations/0163_auto_20211229_1411.py | 35 ------ cookbook/models.py | 18 +-- cookbook/serializer.py | 14 +-- cookbook/signals.py | 10 +- cookbook/tests/api/test_api_food.py | 44 ++----- cookbook/tests/api/test_api_userpreference.py | 31 +++-- cookbook/views/api.py | 3 +- cookbook/views/views.py | 2 +- .../ShoppingListView/ShoppingListView.vue | 6 +- vue/src/components/Badges/Shopping.vue | 12 +- .../components/Modals/GenericModalForm.vue | 46 ++++--- vue/src/components/Modals/TextInput.vue | 56 ++++----- vue/src/locales/en.json | 2 +- vue/src/utils/models.js | 15 +-- vue/src/utils/openapi/api.ts | 114 ++++++++++-------- vue/src/utils/utils.js | 9 +- 18 files changed, 190 insertions(+), 248 deletions(-) delete mode 100644 cookbook/migrations/0163_auto_20211229_1411.py diff --git a/cookbook/forms.py b/cookbook/forms.py index 2497afab..ea86d1cb 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -7,7 +7,7 @@ from django_scopes import scopes_disabled from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField from hcaptcha.fields import hCaptchaField -from .models import (Comment, InviteLink, Keyword, MealPlan, MealType, Recipe, RecipeBook, Food, +from .models import (Comment, Food, InviteLink, Keyword, MealPlan, MealType, Recipe, RecipeBook, RecipeBookEntry, SearchPreference, Space, Storage, Sync, User, UserPreference) @@ -525,7 +525,7 @@ class SpacePreferenceForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # populates the post - self.fields['food_inherit'].queryset = Food.inherit_fields + self.fields['food_inherit'].queryset = Food.inheritable_fields class Meta: model = Space diff --git a/cookbook/migrations/0159_add_shoppinglistentry_fields.py b/cookbook/migrations/0159_add_shoppinglistentry_fields.py index 633d2960..32b0c9ce 100644 --- a/cookbook/migrations/0159_add_shoppinglistentry_fields.py +++ b/cookbook/migrations/0159_add_shoppinglistentry_fields.py @@ -28,11 +28,6 @@ class Migration(migrations.Migration): ] operations = [ - # migrations.AddField( - # model_name='food', - # name='on_hand', - # field=models.BooleanField(default=False), - # ), migrations.AddField( model_name='shoppinglistentry', name='completed_at', @@ -105,11 +100,6 @@ class Migration(migrations.Migration): ], bases=(models.Model, PermissionModelMixin), ), - migrations.AddField( - model_name='food', - name='inherit', - field=models.BooleanField(default=False), - ), migrations.AddField( model_name='userpreference', name='mealplan_autoinclude_related', @@ -117,7 +107,7 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='food', - name='ignore_inherit', + name='inherit_fields', field=models.ManyToManyField(blank=True, to='cookbook.FoodInheritField'), ), migrations.AddField( @@ -145,5 +135,10 @@ class Migration(migrations.Migration): name='shopping_recent_days', field=models.PositiveIntegerField(default=7), ), + migrations.RenameField( + model_name='food', + old_name='ignore_shopping', + new_name='food_onhand', + ), migrations.RunPython(copy_values_to_sle), ] diff --git a/cookbook/migrations/0163_auto_20211229_1411.py b/cookbook/migrations/0163_auto_20211229_1411.py deleted file mode 100644 index f279ca12..00000000 --- a/cookbook/migrations/0163_auto_20211229_1411.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 3.2.10 on 2021-12-29 20:11 - -from django.db import migrations - -from cookbook.models import FoodInheritField - -# TODO this can be deleted -# def temp_migration(apps, schema_editor): -# x = FoodInheritField.objects.get(name='Ignore Shopping', field='ignore_shopping') -# x.name = 'On Hand' -# x.field = 'food_onhand' -# x.save - - -class Migration(migrations.Migration): - - dependencies = [ - ('cookbook', '0162_userpreference_csv_delim'), - ] - - operations = [ - # TODO this can be deleted - # migrations.RunPython(temp_migration), - # TODO this stays - migrations.RenameField( - model_name='food', - old_name='ignore_shopping', - new_name='food_onhand', - ), - # TODO this can be deleted - # migrations.RemoveField( - # model_name='food', - # name='on_hand', - # ), - ] diff --git a/cookbook/models.py b/cookbook/models.py index c5a86f86..bc167ba4 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -482,7 +482,7 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): # exclude fields not implemented yet - inherit_fields = FoodInheritField.objects.exclude(field__in=['diet', 'substitute', 'substitute_children', 'substitute_siblings']) + inheritable_fields = FoodInheritField.objects.exclude(field__in=['diet', 'substitute', 'substitute_children', 'substitute_siblings']) # 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: @@ -492,9 +492,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL) food_onhand = models.BooleanField(default=False) # inherited field description = models.TextField(default='', blank=True) - # on_hand = models.BooleanField(default=False) - inherit = models.BooleanField(default=False) - ignore_inherit = 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) # inherited field: is this name better as inherit instead of ignore inherit? which is more intuitive? space = models.ForeignKey(Space, on_delete=models.CASCADE) objects = ScopedManager(space='space', _manager_class=TreeManager) @@ -512,16 +510,14 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): def reset_inheritance(space=None): # resets inheritted fields to the space defaults and updates all inheritted fields to root object values inherit = space.food_inherit.all() - ignore_inherit = Food.inherit_fields.difference(inherit) + # remove all inherited fields from food + Through = Food.objects.filter(space=space).first().inherit_fields.through + Through.objects.all().delete() # food is going to inherit attributes if space.food_inherit.all().count() > 0: - # using update to avoid creating a N*depth! save signals - Food.objects.filter(space=space).update(inherit=True) # ManyToMany cannot be updated through an UPDATE operation - Through = Food.objects.first().ignore_inherit.through - Through.objects.all().delete() - for i in ignore_inherit: + for i in inherit: Through.objects.bulk_create([ Through(food_id=x, foodinheritfield_id=i.id) for x in Food.objects.filter(space=space).values_list('id', flat=True) @@ -538,8 +534,6 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): 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) - else: # food is not going to inherit any attributes - Food.objects.filter(space=space).update(inherit=False) class Meta: constraints = [ diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 55b87f47..c43b52a7 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -157,15 +157,9 @@ class FoodInheritFieldSerializer(WritableNestedModelSerializer): class UserPreferenceSerializer(serializers.ModelSerializer): - # food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', read_only=True) - food_ignore_default = serializers.SerializerMethodField('get_ignore_default') + food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True) plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True) - # TODO decide: default inherit field values for foods are being handled via VUE client through user preference - # should inherit field instead be set during the django model create? - def get_ignore_default(self, obj): - return FoodInheritFieldSerializer(Food.inherit_fields.difference(obj.space.food_inherit.all()), many=True).data - def create(self, validated_data): if not validated_data.get('user', None): raise ValidationError(_('A user is required')) @@ -181,7 +175,7 @@ class UserPreferenceSerializer(serializers.ModelSerializer): 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_ignore_default', 'default_delay', + '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' ) @@ -376,7 +370,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False) recipe = RecipeSimpleSerializer(allow_null=True, required=False) shopping = serializers.SerializerMethodField('get_shopping_status') - ignore_inherit = FoodInheritFieldSerializer(many=True, allow_null=True, required=False) + inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False) recipe_filter = 'steps__ingredients__food' @@ -403,7 +397,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR model = Food fields = ( 'id', 'name', 'description', 'shopping', 'recipe', 'food_onhand', 'supermarket_category', - 'image', 'parent', 'numchild', 'numrecipe', 'inherit', 'ignore_inherit', 'full_name' + 'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name' ) read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe') diff --git a/cookbook/signals.py b/cookbook/signals.py index 1432dbe3..47e5bf9e 100644 --- a/cookbook/signals.py +++ b/cookbook/signals.py @@ -66,14 +66,14 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs): if not instance: return - inherit = Food.inherit_fields.difference(instance.ignore_inherit.all()) + inherit = instance.inherit_fields.all() # nothing to apply from parent and nothing to apply to children - if (not instance.inherit or not instance.parent or inherit.count() == 0) and instance.numchild == 0: + if (not instance.parent or inherit.count() == 0) and instance.numchild == 0: return inherit = inherit.values_list('field', flat=True) # apply changes from parent to instance for each inheritted field - if instance.inherit and instance.parent and inherit.count() > 0: + if instance.parent and inherit.count() > 0: parent = instance.get_parent() if 'food_onhand' in inherit: instance.food_onhand = parent.food_onhand @@ -89,13 +89,13 @@ 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=True).exclude(ignore_inherit__field='food_onhand'): + for child in instance.get_children().filter(inherit_fields__field='food_onhand'): child.food_onhand = instance.food_onhand _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=True).exclude(ignore_inherit__field='supermarket_category'): + 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): diff --git a/cookbook/tests/api/test_api_food.py b/cookbook/tests/api/test_api_food.py index 21fc9e1c..cde72548 100644 --- a/cookbook/tests/api/test_api_food.py +++ b/cookbook/tests/api/test_api_food.py @@ -57,7 +57,19 @@ def obj_tree_1(request, space_1): except AttributeError: params = {} objs = [] + inherit = params.pop('inherit', False) objs.extend(FoodFactory.create_batch(3, space=space_1, **params)) + + # set all foods to inherit everything + if inherit: + inherit = Food.inheritable_fields + Through = Food.objects.filter(space=space_1).first().inherit_fields.through + for i in inherit: + Through.objects.bulk_create([ + Through(food_id=x, foodinheritfield_id=i.id) + for x in Food.objects.filter(space=space_1).values_list('id', flat=True) + ]) + objs[0].move(objs[1], node_location) objs[1].move(objs[2], node_location) return Food.objects.get(id=objs[1].id) # whenever you move/merge a tree it's safest to re-get the object @@ -496,42 +508,12 @@ def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1): assert (getattr(child, field) == new_val) == inherit -@pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [ - ({'has_category': True, 'inherit': True, }, 'supermarket_category', True, 'cat_1'), - ({'food_onhand': True, 'inherit': True, }, 'food_onhand', True, 'false'), -], indirect=['obj_tree_1']) -# This is more about the model than the API - should this be moved to a different test? -def test_ignoreinherit_field(request, obj_tree_1, field, inherit, new_val, u1_s1): - with scope(space=obj_tree_1.space): - parent = obj_tree_1.get_parent() - child = obj_tree_1.get_descendants()[0] - obj_tree_1.ignore_inherit.add(FoodInheritField.objects.get(field=field)) - new_val = request.getfixturevalue(new_val) - - # change parent to a new value - setattr(parent, field, new_val) - with scope(space=parent.space): - parent.save() # trigger post-save signal - # get the objects again because values are cached - obj_tree_1 = Food.objects.get(id=obj_tree_1.id) - # inheritance is blocked - should not get new value - assert getattr(obj_tree_1, field) != new_val - - setattr(obj_tree_1, field, new_val) - with scope(space=parent.space): - obj_tree_1.save() # trigger post-save signal - # get the objects again because values are cached - child = Food.objects.get(id=child.id) - # inherit with child should still work - assert getattr(child, field) == new_val - - @pytest.mark.parametrize("obj_tree_1", [ ({'has_category': True, 'inherit': False, 'food_onhand': 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.inherit_fields.values_list('id', flat=True)) # set default inherit fields + 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 diff --git a/cookbook/tests/api/test_api_userpreference.py b/cookbook/tests/api/test_api_userpreference.py index b168d707..c31e473f 100644 --- a/cookbook/tests/api/test_api_userpreference.py +++ b/cookbook/tests/api/test_api_userpreference.py @@ -112,30 +112,29 @@ def test_preference_delete(u1_s1, u2_s1): def test_default_inherit_fields(u1_s1, u1_s2, space_1, space_2): - food_inherit_fields = Food.inherit_fields.all() - - r = u1_s1.get( - reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}), - ) + food_inherit_fields = Food.inheritable_fields + assert len([x.field for x in food_inherit_fields]) > 0 # by default space food will not inherit any fields, so all of them will be ignored assert space_1.food_inherit.all().count() == 0 - assert len([x.field for x in food_inherit_fields]) == len([x['field'] for x in json.loads(r.content)['food_ignore_default']]) > 0 - - # inherit all possible fields - with scope(space=space_1): - space_1.food_inherit.add(*Food.inherit_fields.values_list('id', flat=True)) r = u1_s1.get( reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}), ) + assert len([x['field'] for x in json.loads(r.content)['food_inherit_default']]) == 0 - assert space_1.food_inherit.all().count() == Food.inherit_fields.all().count() > 0 - # now by default, food is not ignoring inheritance on any field - assert len([x['field'] for x in json.loads(r.content)['food_ignore_default']]) == 0 + # inherit all possible fields + with scope(space=space_1): + space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True)) - # other spaces and users in those spaced not effected + assert space_1.food_inherit.all().count() == Food.inheritable_fields.count() > 0 + # now by default, food is inheriting all of the possible fields + r = u1_s1.get( + reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}), + ) + assert len([x['field'] for x in json.loads(r.content)['food_inherit_default']]) == space_1.food_inherit.all().count() + + # other spaces and users in those spaces not effected r = u1_s2.get( reverse(DETAIL_URL, args={auth.get_user(u1_s2).id}), ) - assert space_2.food_inherit.all().count() == 0 - assert len([x.field for x in food_inherit_fields]) == len([x['field'] for x in json.loads(r.content)['food_ignore_default']]) > 0 + assert space_2.food_inherit.all().count() == 0 == len([x['field'] for x in json.loads(r.content)['food_inherit_default']]) diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 5f01c401..8b3ff8fb 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -402,7 +402,8 @@ class FoodInheritFieldViewSet(viewsets.ReadOnlyModelViewSet): def get_queryset(self): # exclude fields not yet implemented - return Food.inherit_fields + self.queryset = Food.inheritable_fields + return super().get_queryset() class FoodViewSet(viewsets.ModelViewSet, TreeMixin): diff --git a/cookbook/views/views.py b/cookbook/views/views.py index ef79e072..b875bc38 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -561,7 +561,7 @@ def space(request): space_form = SpacePreferenceForm(instance=request.space) - space_form.base_fields['food_inherit'].queryset = Food.inherit_fields + space_form.base_fields['food_inherit'].queryset = Food.inheritable_fields if request.method == "POST" and 'space_form' in request.POST: form = SpacePreferenceForm(request.POST, prefix='space') if form.is_valid(): diff --git a/vue/src/apps/ShoppingListView/ShoppingListView.vue b/vue/src/apps/ShoppingListView/ShoppingListView.vue index 762f298f..37aa5e2d 100644 --- a/vue/src/apps/ShoppingListView/ShoppingListView.vue +++ b/vue/src/apps/ShoppingListView/ShoppingListView.vue @@ -1012,8 +1012,8 @@ export default { let api = new ApiApiFactory() let ignore_category if (field) { - ignore_category = food.ignore_inherit - .map((x) => food.ignore_inherit.fields) + ignore_category = food.inherit_fields + .map((x) => food.inherit_fields.fields) .flat() .includes(field) } else { @@ -1023,7 +1023,7 @@ export default { return api .partialUpdateFood(food.id, food) .then((result) => { - if (food.inherit && food.supermarket_category && !ignore_category && food.parent) { + if (food.supermarket_category && !ignore_category && food.parent) { makeToast(this.$t("Warning"), this.$t("InheritWarning", { food: food.name }), "warning") } else { StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE) diff --git a/vue/src/components/Badges/Shopping.vue b/vue/src/components/Badges/Shopping.vue index 05a83a2e..57bb4d82 100644 --- a/vue/src/components/Badges/Shopping.vue +++ b/vue/src/components/Badges/Shopping.vue @@ -8,13 +8,13 @@ :class="[shopping ? 'text-success fa-shopping-cart' : 'text-muted fa-cart-plus']" /> - + - {{ $t("Cancel") }} - {{ $t("Confirm") }} + + {{ $t("Cancel") }} + {{ $t("Confirm") }} + @@ -46,7 +46,7 @@ export default { if (this.shopping) { return "shopping" + this.item.id } else { - return "NoDialog" + return "" } }, }, diff --git a/vue/src/components/Modals/GenericModalForm.vue b/vue/src/components/Modals/GenericModalForm.vue index 28d21666..15b1b536 100644 --- a/vue/src/components/Modals/GenericModalForm.vue +++ b/vue/src/components/Modals/GenericModalForm.vue @@ -1,20 +1,18 @@