food inherit tests

This commit is contained in:
smilerz 2021-11-04 16:00:45 -05:00
parent 6c0e979909
commit b92c027919
5 changed files with 202 additions and 21 deletions

View File

@ -824,7 +824,7 @@ class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), mod
class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), models.Model, PermissionModelMixin):
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True, related_name='entries')
food = models.ForeignKey(Food, on_delete=models.CASCADE)
food = models.ForeignKey(Food, on_delete=models.CASCADE, related_name='shopping_entries')
unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True)
ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE, null=True, blank=True)
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)

View File

@ -75,11 +75,20 @@ 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
instance.get_children().filter(inherit=True).exclude(ignore_inherit__field='ignore_shopping').update(ignore_shopping=instance.ignore_shopping)
_save = []
for child in instance.get_children().filter(inherit=True).exclude(ignore_inherit__field='ignore_shopping'):
child.ignore_shopping = instance.ignore_shopping
_save.append(child)
# don't cascade empty supermarket category
if instance.supermarket_category:
instance.get_children().filter(inherit=True).exclude(ignore_inherit__field='supermarket_category').update(supermarket_category=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'):
child.supermarket_category = instance.supermarket_category
_save.append(child)
for child in set(_save):
child.save()
@receiver(post_save, sender=MealPlan)

View File

@ -6,9 +6,10 @@ from django.urls import reverse
from django_scopes import scope, scopes_disabled
from pytest_factoryboy import LazyFixture, register
from cookbook.models import Food, Ingredient, ShoppingList, ShoppingListEntry
from cookbook.models import Food, FoodInheritField, Ingredient, ShoppingList, ShoppingListEntry
from cookbook.tests.conftest import get_random_json_recipe
from cookbook.tests.factories import FoodFactory, IngredientFactory, ShoppingListEntryFactory
from cookbook.tests.factories import (FoodFactory, IngredientFactory, ShoppingListEntryFactory,
SupermarketCategoryFactory)
# ------------------ IMPORTANT -------------------
#
@ -32,18 +33,38 @@ else:
register(FoodFactory, 'obj_1', space=LazyFixture('space_1'))
register(FoodFactory, 'obj_2', space=LazyFixture('space_1'))
register(FoodFactory, 'obj_3', space=LazyFixture('space_2'))
register(SupermarketCategoryFactory, 'cat_1', space=LazyFixture('space_1'))
# @pytest.fixture
# def true():
# return True
@pytest.fixture
def false():
return False
@pytest.fixture
def non_exist():
return {}
@pytest.fixture()
def obj_tree_1(space_1):
def obj_tree_1(request, space_1):
try:
params = request.param
except AttributeError:
params = {}
objs = []
objs.extend(FoodFactory.create_batch(3, space=space_1))
objs.extend(FoodFactory.create_batch(3, space=space_1, **params)) # request.param is a magic variable
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
@ pytest.mark.parametrize("arg", [
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
@ -87,11 +108,11 @@ def test_list_filter(obj_1, obj_2, u1_s1):
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
assert response['count'] == 0
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[4:]}').content)
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[:-4]}').content)
assert response['count'] == 1
@ pytest.mark.parametrize("arg", [
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
@ -116,7 +137,7 @@ def test_update(arg, request, obj_1):
assert response['name'] == 'new'
@ pytest.mark.parametrize("arg", [
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 201],
@ -447,11 +468,63 @@ def test_tree_filter(obj_tree_1, obj_2, obj_3, u1_s1):
assert response['count'] == 4
def test_inherit(obj_tree_1, u1_s1):
pass
# TODO test inherit creating, moving for each field type
# TODO test ignore inherit for each field type
# TODO test with grand-children
# - flow from parent through child and grand-child
# - flow from parent stop when child is ignore inherit
# This is more about the model than the API - should this be moved to a different test?
@pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [
({'add_categories': True, 'inherit': True}, 'supermarket_category', True, 'cat_1'),
({'add_categories': 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'),
], 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):
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
new_val = request.getfixturevalue(new_val)
# if this test passes it demonstrates that inheritance works
# when moving to a parent as each food is created with a different category
assert (getattr(parent, field) == getattr(obj_tree_1, field)) in [inherit, True]
assert (getattr(obj_tree_1, field) == getattr(child, field)) in [inherit, True]
# 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)
child = Food.objects.get(id=child.id)
# when changing parent value the obj value should be same if inherited
assert (getattr(obj_tree_1, field) == new_val) == inherit
assert (getattr(child, field) == new_val) == inherit
# This is more about the model than the API - should this be moved to a different test?
@pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [
({'add_categories': True, 'inherit': True, }, 'supermarket_category', True, 'cat_1'),
({'ignore_shopping': True, 'inherit': True, }, 'ignore_shopping', True, 'false'),
], indirect=['obj_tree_1']) # indirect=True populates magic variable request.param of obj_tree_1 with the parameter
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
# TODO test reset_inheritance

View File

@ -1,10 +1,27 @@
import factory
import pytest
from django.contrib import auth
from django.contrib.auth.models import User
from django_scopes import scopes_disabled
from faker import Factory as FakerFactory
faker = FakerFactory.create()
@pytest.fixture(autouse=True)
def enable_db_access_for_all_tests(db):
pass
@pytest.hookimpl(hookwrapper=True)
def pytest_fixture_setup(fixturedef, request):
if inspect.isgeneratorfunction(fixturedef.func):
yield
else:
with scopes_disabled():
yield
class SpaceFactory(factory.django.DjangoModelFactory):
"""Space factory."""
name = factory.LazyAttribute(lambda x: faker.word())
@ -18,11 +35,92 @@ class SpaceFactory(factory.django.DjangoModelFactory):
model = 'cookbook.Space'
class FoodFactory(factory.django.DjangoModelFactory):
"""Food factory."""
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=3))
class UserFactory(factory.django.DjangoModelFactory):
"""User factory."""
username = factory.LazyAttribute(lambda x: faker.word())
first_name = factory.LazyAttribute(lambda x: faker.first_name())
last_name = factory.LazyAttribute(lambda x: faker.last_name())
email = factory.LazyAttribute(lambda x: faker.email())
space = factory.SubFactory(SpaceFactory)
class Meta:
model = User
class SupermarketCategoryFactory(factory.django.DjangoModelFactory):
"""SupermarketCategory factory."""
name = factory.LazyAttribute(lambda x: faker.word())
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
space = factory.SubFactory(SpaceFactory)
class Meta:
model = 'cookbook.SupermarketCategory'
# @factory.django.mute_signals(post_save)
class FoodFactory(factory.django.DjangoModelFactory):
"""Food factory."""
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=3))
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
supermarket_category = factory.Maybe(
factory.LazyAttribute(
lambda x:
x.add_categories),
yes_declaration=factory.SubFactory(SupermarketCategoryFactory),
no_declaration=None
)
space = factory.SubFactory(SpaceFactory)
# this code will run immediately prior to creating the model object useful when you want a reverse relationship
# log = factory.RelatedFactory(
# UserLogFactory,
# factory_related_name='user',
# action=models.UserLog.ACTION_CREATE,
# )
class Params:
add_categories = False
class Meta:
model = 'cookbook.Food'
class UnitFactory(factory.django.DjangoModelFactory):
"""Unit factory."""
name = factory.LazyAttribute(lambda x: faker.word())
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
space = factory.SubFactory(SpaceFactory)
class Meta:
model = 'cookbook.Unit'
class IngredientFactory(factory.django.DjangoModelFactory):
"""Ingredient factory."""
food = factory.SubFactory(FoodFactory)
unit = factory.SubFactory(UnitFactory)
amount = 1
note = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5))
space = factory.SubFactory(SpaceFactory)
class Meta:
model = 'cookbook.Ingredient'
class ShoppingListEntryFactory(factory.django.DjangoModelFactory):
"""ShoppingListEntry factory."""
# list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True, related_name='entries')
food = factory.SubFactory(FoodFactory)
unit = factory.SubFactory(UnitFactory)
ingredient = factory.SubFactory(IngredientFactory)
amount = 1
order = 0
checked = False
created_by = factory.SubFactory(UserFactory)
# created_at = models.DateTimeField(auto_now_add=True)
# completed_at = models.DateTimeField(null=True, blank=True)
# delay_until = models.DateTimeField(null=True, blank=True)
space = factory.SubFactory(SpaceFactory)
class Meta:
model = 'cookbook.ShoppingListEntry'

View File

@ -225,6 +225,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
root = int(root)
except ValueError:
self.queryset = self.model.objects.none()
if root == 0:
self.queryset = self.model.get_root_nodes()
else: