From 2a5cba01781c14853aae5446d24d8d13e3a25b1e Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Sun, 7 May 2023 00:30:32 +0200 Subject: [PATCH] improvements to property calculation --- cookbook/helper/cache_helper.py | 2 + cookbook/helper/property_helper.py | 14 +++- cookbook/signals.py | 10 ++- cookbook/tests/other/test_food_property.py | 78 ++++++++++++++++++-- cookbook/tests/other/test_unit_conversion.py | 4 + 5 files changed, 94 insertions(+), 14 deletions(-) diff --git a/cookbook/helper/cache_helper.py b/cookbook/helper/cache_helper.py index 3cd7900f9..da903c53b 100644 --- a/cookbook/helper/cache_helper.py +++ b/cookbook/helper/cache_helper.py @@ -2,8 +2,10 @@ class CacheHelper: space = None BASE_UNITS_CACHE_KEY = None + PROPERTY_TYPE_CACHE_KEY = None def __init__(self, space): self.space = space self.BASE_UNITS_CACHE_KEY = f'SPACE_{space.id}_BASE_UNITS' + self.PROPERTY_TYPE_CACHE_KEY = f'SPACE_{space.id}_PROPERTY_TYPES' diff --git a/cookbook/helper/property_helper.py b/cookbook/helper/property_helper.py index ef154fd8f..20ec463ae 100644 --- a/cookbook/helper/property_helper.py +++ b/cookbook/helper/property_helper.py @@ -1,3 +1,6 @@ +from django.core.cache import caches + +from cookbook.helper.cache_helper import CacheHelper from cookbook.helper.unit_conversion_helper import UnitConversionHelper from cookbook.models import PropertyType, Unit, Food, FoodProperty, Recipe, Step @@ -20,15 +23,18 @@ class FoodPropertyHelper: """ ingredients = [] computed_properties = {} - property_types = PropertyType.objects.filter(space=self.space).all() for s in recipe.steps.all(): ingredients += s.ingredients.all() - for fpt in property_types: # TODO is this safe or should I require the request context? - computed_properties[fpt.id] = {'id': fpt.id, 'name': fpt.name, 'icon': fpt.icon, 'description': fpt.description, 'unit': fpt.unit, 'food_values': {}, 'total_value': 0, 'missing_value': False} + property_types = caches['default'].get(CacheHelper(self.space).PROPERTY_TYPE_CACHE_KEY, None) - # TODO unit conversion support + if not property_types: + property_types = PropertyType.objects.filter(space=self.space).all() + caches['default'].set(CacheHelper(self.space).PROPERTY_TYPE_CACHE_KEY, property_types, 60 * 60) # cache is cleared on property type save signal so long duration is fine + + for fpt in property_types: + computed_properties[fpt.id] = {'id': fpt.id, 'name': fpt.name, 'icon': fpt.icon, 'description': fpt.description, 'unit': fpt.unit, 'food_values': {}, 'total_value': 0, 'missing_value': False} uch = UnitConversionHelper(self.space) diff --git a/cookbook/signals.py b/cookbook/signals.py index bce361ea9..f50f0701f 100644 --- a/cookbook/signals.py +++ b/cookbook/signals.py @@ -14,7 +14,7 @@ from cookbook.helper.cache_helper import CacheHelper from cookbook.helper.shopping_helper import RecipeShoppingEditor from cookbook.managers import DICTIONARY from cookbook.models import (Food, FoodInheritField, Ingredient, MealPlan, Recipe, - ShoppingListEntry, Step, UserPreference, SearchPreference, SearchFields, Unit) + ShoppingListEntry, Step, UserPreference, SearchPreference, SearchFields, Unit, PropertyType) SQLITE = True if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', @@ -154,6 +154,12 @@ def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs @receiver(post_save, sender=Unit) -def create_search_preference(sender, instance=None, created=False, **kwargs): +def clear_unit_cache(sender, instance=None, created=False, **kwargs): if instance: caches['default'].delete(CacheHelper(instance.space).BASE_UNITS_CACHE_KEY) + + +@receiver(post_save, sender=PropertyType) +def clear_property_type_cache(sender, instance=None, created=False, **kwargs): + if instance: + caches['default'].delete(CacheHelper(instance.space).PROPERTY_TYPE_CACHE_KEY) diff --git a/cookbook/tests/other/test_food_property.py b/cookbook/tests/other/test_food_property.py index c223ee393..4b2653dd3 100644 --- a/cookbook/tests/other/test_food_property.py +++ b/cookbook/tests/other/test_food_property.py @@ -1,13 +1,17 @@ from django.contrib import auth +from django.core.cache import caches from django_scopes import scopes_disabled +from decimal import Decimal +from cookbook.helper.cache_helper import CacheHelper from cookbook.helper.property_helper import FoodPropertyHelper -from cookbook.models import Unit, Food, PropertyType, FoodProperty, Recipe, Step +from cookbook.models import Unit, Food, PropertyType, FoodProperty, Recipe, Step, UnitConversion -def test_food_property(space_1, u1_s1): +def test_food_property(space_1, space_2, u1_s1): with scopes_disabled(): unit_gram = Unit.objects.create(name='gram', base_unit='g', space=space_1) + unit_kg = Unit.objects.create(name='kg', base_unit='kg', space=space_1) unit_pcs = Unit.objects.create(name='pcs', base_unit='', space=space_1) unit_floz1 = Unit.objects.create(name='fl. oz 1', base_unit='imperial_fluid_ounce', space=space_1) # US and UK use different volume systems (US vs imperial) unit_floz2 = Unit.objects.create(name='fl. oz 2', base_unit='fluid_ounce', space=space_1) @@ -29,6 +33,7 @@ def test_food_property(space_1, u1_s1): food_2_property_nuts = FoodProperty.objects.create(food_amount=100, food_unit=unit_gram, food=food_2, property_amount=0, property_type=property_nuts, space=space_1) food_2_property_price = FoodProperty.objects.create(food_amount=100, food_unit=unit_gram, food=food_2, property_amount=2.50, property_type=property_price, space=space_1) + print('\n----------- TEST PROPERTY - PROPERTY CALCULATION MULTI STEP IDENTICAL UNIT ---------------') recipe_1 = Recipe.objects.create(name='recipe_1', waiting_time=0, working_time=0, space=space_1, created_by=auth.get_user(u1_s1)) step_1 = Step.objects.create(instruction='instruction_step_1', space=space_1) @@ -43,10 +48,67 @@ def test_food_property(space_1, u1_s1): property_values = FoodPropertyHelper(space_1).calculate_recipe_properties(recipe_1) assert property_values[property_fat.id]['name'] == property_fat.name - assert property_values[property_fat.id]['total_value'] == 525 # TODO manually validate those numbers - assert property_values[property_fat.id]['food_values'][food_1.id] == 275 # TODO manually validate those numbers - assert property_values[property_fat.id]['food_values'][food_2.id] == 250 # TODO manually validate those numbers - print(property_values) - # TODO more property tests + assert abs(property_values[property_fat.id]['total_value'] - Decimal(525)) < 0.0001 + assert abs(property_values[property_fat.id]['food_values'][food_1.id]['value'] - Decimal(275)) < 0.0001 + assert abs(property_values[property_fat.id]['food_values'][food_2.id]['value'] - Decimal(250)) < 0.0001 - # TODO test space separation + print('\n----------- TEST PROPERTY - PROPERTY CALCULATION NO POSSIBLE CONVERSION ---------------') + recipe_2 = Recipe.objects.create(name='recipe_2', waiting_time=0, working_time=0, space=space_1, created_by=auth.get_user(u1_s1)) + + step_1 = Step.objects.create(instruction='instruction_step_1', space=space_1) + step_1.ingredients.create(amount=5, unit=unit_pcs, food=food_1, space=space_1) + step_1.ingredients.create(amount=10, unit=unit_pcs, food=food_2, space=space_1) + recipe_2.steps.add(step_1) + + property_values = FoodPropertyHelper(space_1).calculate_recipe_properties(recipe_2) + + assert property_values[property_fat.id]['name'] == property_fat.name + assert abs(property_values[property_fat.id]['total_value']) < 0.0001 + assert abs(property_values[property_fat.id]['food_values'][food_1.id]['value']) < 0.0001 + + print('\n----------- TEST PROPERTY - PROPERTY CALCULATION UNIT CONVERSION ---------------') + uc1 = UnitConversion.objects.create( + base_amount=100, + base_unit=unit_gram, + converted_amount=1, + converted_unit=unit_pcs, + space=space_1, + created_by=auth.get_user(u1_s1), + ) + + property_values = FoodPropertyHelper(space_1).calculate_recipe_properties(recipe_2) + + assert property_values[property_fat.id]['name'] == property_fat.name + assert abs(property_values[property_fat.id]['total_value'] - Decimal(500)) < 0.0001 + assert abs(property_values[property_fat.id]['food_values'][food_1.id]['value'] - Decimal(250)) < 0.0001 + assert abs(property_values[property_fat.id]['food_values'][food_2.id]['value'] - Decimal(250)) < 0.0001 + + print('\n----------- TEST PROPERTY - PROPERTY CALCULATION UNIT CONVERSION MULTIPLE ---------------') + + uc1.delete() + uc1 = UnitConversion.objects.create( + base_amount=0.1, + base_unit=unit_kg, + converted_amount=1, + converted_unit=unit_pcs, + space=space_1, + created_by=auth.get_user(u1_s1), + ) + + property_values = FoodPropertyHelper(space_1).calculate_recipe_properties(recipe_2) + + assert property_values[property_fat.id]['name'] == property_fat.name + assert abs(property_values[property_fat.id]['total_value'] - Decimal(500)) < 0.0001 + assert abs(property_values[property_fat.id]['food_values'][food_1.id]['value'] - Decimal(250)) < 0.0001 + assert abs(property_values[property_fat.id]['food_values'][food_2.id]['value'] - Decimal(250)) < 0.0001 + + print('\n----------- TEST PROPERTY - SPACE SEPARATION ---------------') + + property_fat.space = space_2 + property_fat.save() + + caches['default'].delete(CacheHelper(space_1).PROPERTY_TYPE_CACHE_KEY) # clear cache as objects won't change space in reality + + property_values = FoodPropertyHelper(space_1).calculate_recipe_properties(recipe_2) + + assert property_fat.id not in property_values diff --git a/cookbook/tests/other/test_unit_conversion.py b/cookbook/tests/other/test_unit_conversion.py index 917f5991c..a66eaf6ba 100644 --- a/cookbook/tests/other/test_unit_conversion.py +++ b/cookbook/tests/other/test_unit_conversion.py @@ -138,6 +138,10 @@ def test_unit_conversions(space_1, space_2, u1_s1): assert len(uch.get_conversions(ingredient_food_2_pcs)) == 1 print(uch.get_conversions(ingredient_food_2_pcs)) + print('\n----------- TEST CUSTOM CONVERSION - CONVERT MULTI STEP ---------------') + + # TODO add test for multi step conversion ... do I even do or want to support this ? + print('\n----------- TEST CUSTOM CONVERSION - REVERSE CONVERSION ---------------') uc2 = UnitConversion.objects.create( base_amount=200,