-
-
Notifications
You must be signed in to change notification settings - Fork 604
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add some additional basic nutritional values validation for ingredients #1726
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,9 @@ | |
) | ||
from typing import Optional | ||
|
||
# wger | ||
from wger.nutrition.consts import ENERGY_FACTOR | ||
|
||
|
||
@dataclass | ||
class IngredientData: | ||
|
@@ -50,6 +53,7 @@ def sanity_checks(self): | |
self.brand = self.brand[:200] | ||
self.common_name = self.common_name[:200] | ||
|
||
# Mass checks (not more than 100g of something per 100g of product etc) | ||
macros = [ | ||
'protein', | ||
'fat', | ||
|
@@ -64,8 +68,44 @@ def sanity_checks(self): | |
if value and value > 100: | ||
raise ValueError(f'Value for {macro} is greater than 100: {value}') | ||
|
||
if self.fat_saturated and self.fat_saturated > self.fat: | ||
raise ValueError( | ||
f'Saturated fat is greater than fat: {self.fat_saturated} > {self.fat}' | ||
) | ||
|
||
if self.carbohydrates_sugar and self.carbohydrates_sugar > self.carbohydrates: | ||
raise ValueError( | ||
f'Sugar is greater than carbohydrates: {self.carbohydrates_sugar} > {self.carbohydrates}' | ||
) | ||
|
||
if self.carbohydrates + self.protein + self.fat > 100: | ||
raise ValueError(f'Total of carbohydrates, protein and fat is greater than 100!') | ||
|
||
# Energy approximations | ||
energy_protein = self.protein * ENERGY_FACTOR['protein']['metric'] | ||
energy_carbohydrates = self.carbohydrates * ENERGY_FACTOR['carbohydrates']['metric'] | ||
energy_fat = self.fat * ENERGY_FACTOR['fat']['metric'] | ||
energy_calculated = energy_protein + energy_carbohydrates + energy_fat | ||
|
||
if energy_fat > self.energy: | ||
raise ValueError( | ||
f'Energy calculated from fat is greater than total energy: {energy_fat} > {self.energy}' | ||
) | ||
|
||
if energy_carbohydrates > self.energy: | ||
raise ValueError( | ||
f'Energy calculated from carbohydrates is greater than total energy: {energy_carbohydrates} > {self.energy}' | ||
) | ||
|
||
if energy_protein > self.energy: | ||
raise ValueError( | ||
f'Energy calculated from protein is greater than total energy: {energy_protein} > {self.energy}' | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The protein, carb and fat energy values are rough. Checking this too strictly might trigger false positives , especially for ingredients that only contain 1 macro (eg egg whites) For sanity checking maybe allow 20% error or so ? I would be curious to have a look at ingredients for which we trigger errors here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also all these checks are quite verbose. Do we need to raise ValueErrors explicitly? Could we replace these with simple, single line assert statements ? If the code is more obvious, it also reduces the need for unit testing |
||
|
||
if energy_calculated > self.energy: | ||
raise ValueError( | ||
f'Total energy calculated is greater than energy: {energy_calculated} > {self.energy}' | ||
) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also this is not fool proof. Btw I thought we already had such a check somewhere There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note also that in current form, if this triggers, one of the previous ones will have triggered too. |
||
def dict(self): | ||
return asdict(self) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
# This file is part of wger Workout Manager. | ||
# | ||
# wger Workout Manager is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU Affero General Public License as published by | ||
# the Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# wger Workout Manager is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU Affero General Public License | ||
# along with Workout Manager. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
# Django | ||
from django.test import SimpleTestCase | ||
|
||
# wger | ||
from wger.nutrition.dataclasses import IngredientData | ||
from wger.utils.constants import CC_0_LICENSE_ID | ||
|
||
|
||
class IngredientDataclassTestCase(SimpleTestCase): | ||
""" | ||
Test validation rules | ||
""" | ||
|
||
ingredient_data: IngredientData | ||
|
||
def setUp(self): | ||
self.ingredient_data = IngredientData( | ||
name='Foo With Chocolate', | ||
remote_id='1234567', | ||
language_id=1, | ||
energy=166.0, | ||
protein=32.1, | ||
carbohydrates=0.0, | ||
carbohydrates_sugar=None, | ||
fat=3.24, | ||
fat_saturated=None, | ||
fiber=None, | ||
sodium=None, | ||
code=None, | ||
source_name='USDA', | ||
source_url='', | ||
common_name='', | ||
brand='', | ||
license_id=CC_0_LICENSE_ID, | ||
license_author='', | ||
license_title='', | ||
license_object_url='', | ||
) | ||
|
||
def test_validation_ok(self): | ||
"""""" | ||
self.assertEqual(self.ingredient_data.sanity_checks(), None) | ||
|
||
def test_validation_bigger_100(self): | ||
""" | ||
Test the validation for values bigger than 100 | ||
""" | ||
self.ingredient_data.protein = 101 | ||
self.assertRaises(ValueError, self.ingredient_data.sanity_checks) | ||
|
||
def test_validation_saturated_fat(self): | ||
""" | ||
Test the validation for saturated fat | ||
""" | ||
self.ingredient_data.fat = 20 | ||
self.ingredient_data.fat_saturated = 30 | ||
self.assertRaises(ValueError, self.ingredient_data.sanity_checks) | ||
|
||
def test_validation_sugar(self): | ||
""" | ||
Test the validation for sugar | ||
""" | ||
self.ingredient_data.carbohydrates = 20 | ||
self.ingredient_data.carbohydrates_sugar = 30 | ||
self.assertRaises(ValueError, self.ingredient_data.sanity_checks) | ||
|
||
def test_validation_energy_fat(self): | ||
""" | ||
Test the validation for energy and fat | ||
""" | ||
self.ingredient_data.energy = 200 | ||
self.ingredient_data.fat = 30 # generates 30 * 9 = 270 kcal | ||
self.assertRaisesRegex( | ||
ValueError, | ||
'Energy calculated from fat', | ||
self.ingredient_data.sanity_checks, | ||
) | ||
|
||
def test_validation_energy_protein(self): | ||
""" | ||
Test the validation for energy and protein | ||
""" | ||
self.ingredient_data.energy = 100 | ||
self.ingredient_data.protein = 30 # generates 30 * 4 = 120 kcal | ||
self.assertRaisesRegex( | ||
ValueError, | ||
'Energy calculated from protein', | ||
self.ingredient_data.sanity_checks, | ||
) | ||
|
||
def test_validation_energy_carbohydrates(self): | ||
""" | ||
Test the validation for energy and carbohydrates | ||
""" | ||
self.ingredient_data.energy = 100 | ||
self.ingredient_data.carbohydrates = 30 # generates 30 * 4 = 120 kcal | ||
self.assertRaisesRegex( | ||
ValueError, | ||
'Energy calculated from carbohydrates', | ||
self.ingredient_data.sanity_checks, | ||
) | ||
|
||
def test_validation_energy_total(self): | ||
""" | ||
Test the validation for energy total | ||
""" | ||
self.ingredient_data.energy = 200 # less than 120 + 80 + 90 | ||
self.ingredient_data.protein = 30 # generates 30 * 4 = 120 kcal | ||
self.ingredient_data.carbohydrates = 20 # generates 20 * 4 = 80 kcal | ||
self.ingredient_data.fat = 10 # generates 10 * 9 = 90 kcal | ||
self.assertRaisesRegex( | ||
ValueError, | ||
'Total energy calculated', | ||
self.ingredient_data.sanity_checks, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we replacing clear explicit units with abstract opaque terms?