improved converters and helpers
This commit is contained in:
9
cookbook/helper/cache_helper.py
Normal file
9
cookbook/helper/cache_helper.py
Normal file
@ -0,0 +1,9 @@
|
||||
class CacheHelper:
|
||||
space = None
|
||||
|
||||
BASE_UNITS_CACHE_KEY = None
|
||||
|
||||
def __init__(self, space):
|
||||
self.space = space
|
||||
|
||||
self.BASE_UNITS_CACHE_KEY = f'SPACE_{space.id}_BASE_UNITS'
|
@ -1,34 +1,51 @@
|
||||
from cookbook.models import FoodPropertyType
|
||||
|
||||
|
||||
def calculate_recipe_properties(recipe):
|
||||
ingredients = []
|
||||
computed_properties = {}
|
||||
property_types = FoodPropertyType.objects.filter(space=recipe.space).all()
|
||||
class FoodPropertyHelper:
|
||||
space = None
|
||||
|
||||
for s in recipe.steps.all():
|
||||
ingredients += s.ingredients.all()
|
||||
def __init__(self, space):
|
||||
"""
|
||||
Helper to perform food property calculations
|
||||
:param space: space to limit scope to
|
||||
"""
|
||||
self.space = space
|
||||
|
||||
for fpt in property_types: # TODO is this safe or should I require the request context?
|
||||
computed_properties[fpt.id] = {'name': fpt.name, 'food_values': {}, 'total_value': 0}
|
||||
def calculate_recipe_properties(self, recipe):
|
||||
"""
|
||||
Calculate all food properties for a given recipe.
|
||||
:param recipe: recipe to calculate properties for
|
||||
:return: dict of with property keys and total/food values for each property available
|
||||
"""
|
||||
ingredients = []
|
||||
computed_properties = {}
|
||||
property_types = FoodPropertyType.objects.filter(space=self.space).all()
|
||||
|
||||
# TODO unit conversion support
|
||||
for s in recipe.steps.all():
|
||||
ingredients += s.ingredients.all()
|
||||
|
||||
for i in ingredients:
|
||||
for pt in property_types:
|
||||
p = i.food.foodproperty_set.filter(property_type=pt).first()
|
||||
if p:
|
||||
computed_properties[p.property_type.id]['total_value'] += (i.amount / p.food_amount) * p.property_amount
|
||||
computed_properties[p.property_type.id]['food_values'] = add_or_create(computed_properties[p.property_type.id]['food_values'], i.food.id, (i.amount / p.food_amount) * p.property_amount)
|
||||
else:
|
||||
computed_properties[pt.id]['food_values'][i.food.id] = None
|
||||
for fpt in property_types: # TODO is this safe or should I require the request context?
|
||||
computed_properties[fpt.id] = {'name': fpt.name, 'food_values': {}, 'total_value': 0}
|
||||
|
||||
return computed_properties
|
||||
# TODO unit conversion support
|
||||
|
||||
# small dict helper to add to existing key or create new, probably a better way of doing this
|
||||
def add_or_create(d, key, value):
|
||||
if key in d:
|
||||
d[key] += value
|
||||
else:
|
||||
d[key] = value
|
||||
return d
|
||||
for i in ingredients:
|
||||
for pt in property_types:
|
||||
p = i.food.foodproperty_set.filter(space=self.space, property_type=pt).first()
|
||||
if p:
|
||||
computed_properties[p.property_type.id]['total_value'] += (i.amount / p.food_amount) * p.property_amount
|
||||
computed_properties[p.property_type.id]['food_values'] = self.add_or_create(computed_properties[p.property_type.id]['food_values'], i.food.id, (i.amount / p.food_amount) * p.property_amount)
|
||||
else:
|
||||
computed_properties[pt.id]['food_values'][i.food.id] = None
|
||||
|
||||
return computed_properties
|
||||
|
||||
# small dict helper to add to existing key or create new, probably a better way of doing this
|
||||
# TODO move to central helper ?
|
||||
@staticmethod
|
||||
def add_or_create(d, key, value):
|
||||
if key in d:
|
||||
d[key] += value
|
||||
else:
|
||||
d[key] = value
|
||||
return d
|
||||
|
@ -1,7 +1,11 @@
|
||||
from django.core.cache import caches
|
||||
from pint import UnitRegistry, UndefinedUnitError, PintError
|
||||
|
||||
from cookbook.helper.cache_helper import CacheHelper
|
||||
from cookbook.models import Ingredient, Unit
|
||||
|
||||
# basic units that should be considered for "to" conversions
|
||||
# TODO possible remove this hardcoded units and just add a flag to the unit
|
||||
CONVERT_TO_UNITS = {
|
||||
'metric': ['g', 'kg', 'ml', 'l'],
|
||||
'us': ['ounce', 'pound', 'fluid_ounce', 'pint', 'quart', 'gallon'],
|
||||
@ -9,69 +13,102 @@ CONVERT_TO_UNITS = {
|
||||
}
|
||||
|
||||
|
||||
def base_conversions(ingredient_list):
|
||||
ureg = UnitRegistry()
|
||||
pint_converted_list = ingredient_list.copy()
|
||||
for i in ingredient_list:
|
||||
try:
|
||||
conversion_unit = i.unit.name
|
||||
if i.unit.base_unit:
|
||||
conversion_unit = i.unit.base_unit
|
||||
quantitiy = ureg.Quantity(f'{i.amount} {conversion_unit}')
|
||||
class UnitConversionHelper:
|
||||
space = None
|
||||
|
||||
# TODO allow setting which units to convert to
|
||||
units = Unit.objects.filter(base_unit__in=(CONVERT_TO_UNITS['metric'] + CONVERT_TO_UNITS['us'] + CONVERT_TO_UNITS['uk'])).all()
|
||||
def __init__(self, space):
|
||||
"""
|
||||
Initializes unit conversion helper
|
||||
:param space: space to perform conversions on
|
||||
"""
|
||||
self.space = space
|
||||
|
||||
for u in units:
|
||||
try:
|
||||
converted = quantitiy.to(u.base_unit)
|
||||
ingredient = Ingredient(amount=converted.m, unit=u, food=ingredient_list[0].food, )
|
||||
if not any((x.unit.name == ingredient.unit.name or x.unit.base_unit == ingredient.unit.name) for x in pint_converted_list):
|
||||
pint_converted_list.append(ingredient)
|
||||
except PintError:
|
||||
pass
|
||||
except PintError:
|
||||
pass
|
||||
def base_conversions(self, ingredient_list):
|
||||
"""
|
||||
Calculates all possible base unit conversions for each ingredient give.
|
||||
Converts to all common base units IF they exist in the unit database of the space.
|
||||
For useful results all ingredients passed should be of the same food, otherwise filtering afterwards might be required.
|
||||
:param ingredient_list: list of ingredients to convert
|
||||
:return: ingredient list with appended conversions
|
||||
"""
|
||||
ureg = UnitRegistry()
|
||||
pint_converted_list = ingredient_list.copy()
|
||||
for i in ingredient_list:
|
||||
try:
|
||||
conversion_unit = i.unit.name
|
||||
if i.unit.base_unit:
|
||||
conversion_unit = i.unit.base_unit
|
||||
quantitiy = ureg.Quantity(f'{i.amount} {conversion_unit}')
|
||||
|
||||
return pint_converted_list
|
||||
# TODO allow setting which units to convert to? possibly only once conversions become visible
|
||||
units = caches['default'].get(CacheHelper(self.space).BASE_UNITS_CACHE_KEY, None)
|
||||
if not units:
|
||||
units = Unit.objects.filter(space=self.space, base_unit__in=(CONVERT_TO_UNITS['metric'] + CONVERT_TO_UNITS['us'] + CONVERT_TO_UNITS['uk'])).all()
|
||||
caches['default'].set(CacheHelper(self.space).BASE_UNITS_CACHE_KEY, units, 60 * 60) # cache is cleared on unit save signal so long duration is fine
|
||||
|
||||
for u in units:
|
||||
try:
|
||||
converted = quantitiy.to(u.base_unit)
|
||||
ingredient = Ingredient(amount=converted.m, unit=u, food=ingredient_list[0].food, )
|
||||
if not any((x.unit.name == ingredient.unit.name or x.unit.base_unit == ingredient.unit.name) for x in pint_converted_list):
|
||||
pint_converted_list.append(ingredient)
|
||||
except PintError:
|
||||
pass
|
||||
except PintError:
|
||||
pass
|
||||
|
||||
def get_conversions(ingredient):
|
||||
conversions = [ingredient]
|
||||
if ingredient.unit:
|
||||
for c in ingredient.unit.unit_conversion_base_relation.all():
|
||||
r = _uc_convert(c, ingredient.amount, ingredient.unit, ingredient.food)
|
||||
if r and r not in conversions:
|
||||
conversions.append(r)
|
||||
for c in ingredient.unit.unit_conversion_converted_relation.all():
|
||||
r = _uc_convert(c, ingredient.amount, ingredient.unit, ingredient.food)
|
||||
if r and r not in conversions:
|
||||
conversions.append(r)
|
||||
return pint_converted_list
|
||||
|
||||
conversions = base_conversions(conversions)
|
||||
def get_conversions(self, ingredient):
|
||||
"""
|
||||
Converts an ingredient to all possible conversions based on the custom unit conversion database.
|
||||
After that passes conversion to UnitConversionHelper.base_conversions() to get all base conversions possible.
|
||||
:param ingredient: Ingredient object
|
||||
:return: list of ingredients with all possible custom and base conversions
|
||||
"""
|
||||
conversions = [ingredient]
|
||||
if ingredient.unit:
|
||||
for c in ingredient.unit.unit_conversion_base_relation.filter(space=self.space).all():
|
||||
r = self._uc_convert(c, ingredient.amount, ingredient.unit, ingredient.food)
|
||||
if r and r not in conversions:
|
||||
conversions.append(r)
|
||||
for c in ingredient.unit.unit_conversion_converted_relation.filter(space=self.space).all():
|
||||
r = self._uc_convert(c, ingredient.amount, ingredient.unit, ingredient.food)
|
||||
if r and r not in conversions:
|
||||
conversions.append(r)
|
||||
|
||||
return conversions
|
||||
conversions = self.base_conversions(conversions)
|
||||
|
||||
return conversions
|
||||
|
||||
def _uc_convert(uc, amount, unit, food):
|
||||
if uc.food is None or uc.food == food:
|
||||
if unit == uc.base_unit:
|
||||
return Ingredient(amount=amount * (uc.converted_amount / uc.base_amount), unit=uc.converted_unit, food=food)
|
||||
# return {
|
||||
# 'amount': amount * (uc.converted_amount / uc.base_amount),
|
||||
# 'unit': {
|
||||
# 'id': uc.converted_unit.id,
|
||||
# 'name': uc.converted_unit.name,
|
||||
# 'plural_name': uc.converted_unit.plural_name
|
||||
# },
|
||||
# }
|
||||
else:
|
||||
return Ingredient(amount=amount * (uc.base_amount / uc.converted_amount), unit=uc.base_unit, food=food)
|
||||
# return {
|
||||
# 'amount': amount * (uc.base_amount / uc.converted_amount),
|
||||
# 'unit': {
|
||||
# 'id': uc.base_unit.id,
|
||||
# 'name': uc.base_unit.name,
|
||||
# 'plural_name': uc.base_unit.plural_name
|
||||
# },
|
||||
# }
|
||||
def _uc_convert(self, uc, amount, unit, food):
|
||||
"""
|
||||
Helper to calculate values for custom unit conversions.
|
||||
Converts given base values using the passed UnitConversion object into a converted Ingredient
|
||||
:param uc: UnitConversion object
|
||||
:param amount: base amount
|
||||
:param unit: base unit
|
||||
:param food: base food
|
||||
:return: converted ingredient object from base amount/unit/food
|
||||
"""
|
||||
if uc.food is None or uc.food == food:
|
||||
if unit == uc.base_unit:
|
||||
return Ingredient(amount=amount * (uc.converted_amount / uc.base_amount), unit=uc.converted_unit, food=food, space=self.space)
|
||||
# return {
|
||||
# 'amount': amount * (uc.converted_amount / uc.base_amount),
|
||||
# 'unit': {
|
||||
# 'id': uc.converted_unit.id,
|
||||
# 'name': uc.converted_unit.name,
|
||||
# 'plural_name': uc.converted_unit.plural_name
|
||||
# },
|
||||
# }
|
||||
else:
|
||||
return Ingredient(amount=amount * (uc.base_amount / uc.converted_amount), unit=uc.base_unit, food=food, space=self.space)
|
||||
# return {
|
||||
# 'amount': amount * (uc.base_amount / uc.converted_amount),
|
||||
# 'unit': {
|
||||
# 'id': uc.base_unit.id,
|
||||
# 'name': uc.base_unit.name,
|
||||
# 'plural_name': uc.base_unit.plural_name
|
||||
# },
|
||||
# }
|
||||
|
@ -4,15 +4,17 @@ from functools import wraps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.postgres.search import SearchVector
|
||||
from django.core.cache import caches
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import translation
|
||||
from django_scopes import scope, scopes_disabled
|
||||
|
||||
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)
|
||||
ShoppingListEntry, Step, UserPreference, SearchPreference, SearchFields, Unit)
|
||||
|
||||
SQLITE = True
|
||||
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
|
||||
@ -149,3 +151,9 @@ def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs
|
||||
print("MEAL_AUTO_ADD Created SLR")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
@receiver(post_save, sender=Unit)
|
||||
def create_search_preference(sender, instance=None, created=False, **kwargs):
|
||||
if instance:
|
||||
caches['default'].delete(CacheHelper(instance.space).BASE_UNITS_CACHE_KEY)
|
||||
|
@ -1,9 +1,8 @@
|
||||
from django.contrib import auth
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.helper.food_property_helper import calculate_recipe_properties
|
||||
from cookbook.helper.unit_conversion_helper import get_conversions
|
||||
from cookbook.models import Unit, Food, Ingredient, UnitConversion, FoodPropertyType, FoodProperty, Recipe, Step
|
||||
from cookbook.helper.food_property_helper import FoodPropertyHelper
|
||||
from cookbook.models import Unit, Food, FoodPropertyType, FoodProperty, Recipe, Step
|
||||
|
||||
|
||||
def test_food_property(space_1, u1_s1):
|
||||
@ -41,7 +40,7 @@ def test_food_property(space_1, u1_s1):
|
||||
step_2.ingredients.create(amount=50, unit=unit_gram, food=food_1, space=space_1)
|
||||
recipe_1.steps.add(step_2)
|
||||
|
||||
property_values = calculate_recipe_properties(recipe_1)
|
||||
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
|
||||
@ -49,3 +48,5 @@ def test_food_property(space_1, u1_s1):
|
||||
assert property_values[property_fat.id]['food_values'][food_2.id] == 250 # TODO manually validate those numbers
|
||||
print(property_values)
|
||||
# TODO more property tests
|
||||
|
||||
# TODO test space separation
|
||||
|
@ -3,12 +3,15 @@ from _decimal import Decimal
|
||||
from django.contrib import auth
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.helper.unit_conversion_helper import get_conversions
|
||||
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
|
||||
from cookbook.models import Unit, Food, Ingredient, UnitConversion
|
||||
|
||||
|
||||
def test_unit_conversions(space_1, u1_s1):
|
||||
def test_unit_conversions(space_1, space_2, u1_s1):
|
||||
with scopes_disabled():
|
||||
uch = UnitConversionHelper(space_1)
|
||||
uch_space_2 = UnitConversionHelper(space_2)
|
||||
|
||||
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)
|
||||
@ -27,7 +30,7 @@ def test_unit_conversions(space_1, u1_s1):
|
||||
space=space_1,
|
||||
)
|
||||
|
||||
conversions = get_conversions(ingredient_food_1_gram)
|
||||
conversions = uch.get_conversions(ingredient_food_1_gram)
|
||||
print(conversions)
|
||||
assert len(conversions) == 2
|
||||
assert next(x for x in conversions if x.unit == unit_kg) is not None
|
||||
@ -42,7 +45,7 @@ def test_unit_conversions(space_1, u1_s1):
|
||||
space=space_1,
|
||||
)
|
||||
|
||||
conversions = get_conversions(ingredient_food_1_floz1)
|
||||
conversions = uch.get_conversions(ingredient_food_1_floz1)
|
||||
assert len(conversions) == 2
|
||||
assert next(x for x in conversions if x.unit == unit_floz2) is not None
|
||||
assert next(x for x in conversions if x.unit == unit_floz2).amount == 96.07599404038842 # TODO validate value
|
||||
@ -50,7 +53,7 @@ def test_unit_conversions(space_1, u1_s1):
|
||||
print(conversions)
|
||||
|
||||
unit_pint = Unit.objects.create(name='pint', base_unit='pint', space=space_1)
|
||||
conversions = get_conversions(ingredient_food_1_floz1)
|
||||
conversions = uch.get_conversions(ingredient_food_1_floz1)
|
||||
assert len(conversions) == 3
|
||||
assert next(x for x in conversions if x.unit == unit_pint) is not None
|
||||
assert next(x for x in conversions if x.unit == unit_pint).amount == 6.004749627524276 # TODO validate value
|
||||
@ -66,7 +69,7 @@ def test_unit_conversions(space_1, u1_s1):
|
||||
space=space_1,
|
||||
created_by=auth.get_user(u1_s1),
|
||||
)
|
||||
conversions = get_conversions(ingredient_food_1_gram)
|
||||
conversions = uch.get_conversions(ingredient_food_1_gram)
|
||||
|
||||
assert len(conversions) == 3
|
||||
assert next(x for x in conversions if x.unit == unit_fantasy) is not None
|
||||
@ -89,10 +92,10 @@ def test_unit_conversions(space_1, u1_s1):
|
||||
space=space_1,
|
||||
)
|
||||
|
||||
assert len(get_conversions(ingredient_food_1_pcs)) == 1
|
||||
assert len(get_conversions(ingredient_food_2_pcs)) == 1
|
||||
print(get_conversions(ingredient_food_1_pcs))
|
||||
print(get_conversions(ingredient_food_2_pcs))
|
||||
assert len(uch.get_conversions(ingredient_food_1_pcs)) == 1
|
||||
assert len(uch.get_conversions(ingredient_food_2_pcs)) == 1
|
||||
print(uch.get_conversions(ingredient_food_1_pcs))
|
||||
print(uch.get_conversions(ingredient_food_2_pcs))
|
||||
|
||||
print('\n----------- TEST CUSTOM CONVERSION - PCS TO MULTIPLE BASE ---------------')
|
||||
uc1 = UnitConversion.objects.create(
|
||||
@ -105,14 +108,14 @@ def test_unit_conversions(space_1, u1_s1):
|
||||
created_by=auth.get_user(u1_s1),
|
||||
)
|
||||
|
||||
conversions = get_conversions(ingredient_food_1_pcs)
|
||||
conversions = uch.get_conversions(ingredient_food_1_pcs)
|
||||
assert len(conversions) == 3
|
||||
assert next(x for x in conversions if x.unit == unit_gram).amount == 1000
|
||||
assert next(x for x in conversions if x.unit == unit_kg).amount == 1
|
||||
print(conversions)
|
||||
|
||||
assert len(get_conversions(ingredient_food_2_pcs)) == 1
|
||||
print(get_conversions(ingredient_food_2_pcs))
|
||||
assert len(uch.get_conversions(ingredient_food_2_pcs)) == 1
|
||||
print(uch.get_conversions(ingredient_food_2_pcs))
|
||||
|
||||
print('\n----------- TEST CUSTOM CONVERSION - REVERSE CONVERSION ---------------')
|
||||
uc2 = UnitConversion.objects.create(
|
||||
@ -125,14 +128,35 @@ def test_unit_conversions(space_1, u1_s1):
|
||||
created_by=auth.get_user(u1_s1),
|
||||
)
|
||||
|
||||
conversions = get_conversions(ingredient_food_1_pcs)
|
||||
conversions = uch.get_conversions(ingredient_food_1_pcs)
|
||||
assert len(conversions) == 3
|
||||
assert next(x for x in conversions if x.unit == unit_gram).amount == 1000
|
||||
assert next(x for x in conversions if x.unit == unit_kg).amount == 1
|
||||
print(conversions)
|
||||
|
||||
conversions = get_conversions(ingredient_food_2_pcs)
|
||||
conversions = uch.get_conversions(ingredient_food_2_pcs)
|
||||
assert len(conversions) == 3
|
||||
assert next(x for x in conversions if x.unit == unit_gram).amount == 1000
|
||||
assert next(x for x in conversions if x.unit == unit_kg).amount == 1
|
||||
print(conversions)
|
||||
|
||||
print('\n----------- TEST SPACE SEPARATION ---------------')
|
||||
uc2.space = space_2
|
||||
uc2.save()
|
||||
conversions = uch.get_conversions(ingredient_food_2_pcs)
|
||||
assert len(conversions) == 1
|
||||
print(conversions)
|
||||
|
||||
conversions = uch_space_2.get_conversions(ingredient_food_1_gram)
|
||||
assert len(conversions) == 1
|
||||
assert not any(x for x in conversions if x.unit == unit_kg)
|
||||
print(conversions)
|
||||
|
||||
unit_kg_space_2 = Unit.objects.create(name='kg', base_unit='kg', space=space_2)
|
||||
conversions = uch_space_2.get_conversions(ingredient_food_1_gram)
|
||||
assert len(conversions) == 2
|
||||
assert not any(x for x in conversions if x.unit == unit_kg)
|
||||
assert next(x for x in conversions if x.unit == unit_kg_space_2) is not None
|
||||
assert next(x for x in conversions if x.unit == unit_kg_space_2).amount == 0.1
|
||||
print(conversions)
|
||||
|
||||
|
Reference in New Issue
Block a user