From a2b987352fbd77dcf3a54160a1bb1ce7049ed5b0 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Fri, 24 Feb 2023 22:12:52 +0100 Subject: [PATCH] user nutrition types + ingredient nutrtion calculation --- cookbook/admin.py | 16 +++++++- ...itconversion.py => 0189_unitconversion.py} | 4 +- .../0190_nutritiontype_foodnutrition.py | 40 +++++++++++++++++++ cookbook/models.py | 35 ++++++++++++---- cookbook/serializer.py | 24 +++++++++-- 5 files changed, 105 insertions(+), 14 deletions(-) rename cookbook/migrations/{0187_unitconversion.py => 0189_unitconversion.py} (93%) create mode 100644 cookbook/migrations/0190_nutritiontype_foodnutrition.py diff --git a/cookbook/admin.py b/cookbook/admin.py index 9b10c7c1..8aab4fdd 100644 --- a/cookbook/admin.py +++ b/cookbook/admin.py @@ -15,7 +15,7 @@ from .models import (BookmarkletImport, Comment, CookLog, Food, FoodInheritField Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, - TelegramBot, Unit, UserFile, UserPreference, ViewLog, Automation, UserSpace, UnitConversion) + TelegramBot, Unit, UserFile, UserPreference, ViewLog, Automation, UserSpace, UnitConversion, NutritionType, FoodNutrition) class CustomUserAdmin(UserAdmin): @@ -327,6 +327,20 @@ class ShareLinkAdmin(admin.ModelAdmin): admin.site.register(ShareLink, ShareLinkAdmin) +class NutritionTypeAdmin(admin.ModelAdmin): + list_display = ('id', 'name') + + +admin.site.register(NutritionType, NutritionTypeAdmin) + + +class FoodNutritionAdmin(admin.ModelAdmin): + list_display = ('id', 'food_amount', 'food_unit', 'food', 'nutrition_amount', 'nutrition_type') + + +admin.site.register(FoodNutrition, FoodNutritionAdmin) + + class NutritionInformationAdmin(admin.ModelAdmin): list_display = ('id',) diff --git a/cookbook/migrations/0187_unitconversion.py b/cookbook/migrations/0189_unitconversion.py similarity index 93% rename from cookbook/migrations/0187_unitconversion.py rename to cookbook/migrations/0189_unitconversion.py index a6c152b7..257ab0f3 100644 --- a/cookbook/migrations/0187_unitconversion.py +++ b/cookbook/migrations/0189_unitconversion.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.4 on 2023-01-09 16:41 +# Generated by Django 4.1.7 on 2023-02-24 19:47 import cookbook.models from django.conf import settings @@ -11,7 +11,7 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('cookbook', '0186_automation_order_alter_automation_type'), + ('cookbook', '0188_space_no_sharing_limit'), ] operations = [ diff --git a/cookbook/migrations/0190_nutritiontype_foodnutrition.py b/cookbook/migrations/0190_nutritiontype_foodnutrition.py new file mode 100644 index 00000000..dd69466c --- /dev/null +++ b/cookbook/migrations/0190_nutritiontype_foodnutrition.py @@ -0,0 +1,40 @@ +# Generated by Django 4.1.7 on 2023-02-24 20:15 + +import cookbook.models +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0189_unitconversion'), + ] + + operations = [ + migrations.CreateModel( + name='NutritionType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128)), + ('unit', models.CharField(blank=True, max_length=64, null=True)), + ('icon', models.CharField(blank=True, max_length=16, null=True)), + ('description', models.CharField(blank=True, max_length=512, null=True)), + ('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')), + ], + bases=(models.Model, cookbook.models.PermissionModelMixin), + ), + migrations.CreateModel( + name='FoodNutrition', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('food_amount', models.DecimalField(decimal_places=2, default=0, max_digits=32)), + ('nutrition_amount', models.DecimalField(decimal_places=4, default=0, max_digits=32)), + ('food', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.food')), + ('food_unit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.unit')), + ('nutrition_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cookbook.nutritiontype')), + ('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')), + ], + bases=(models.Model, cookbook.models.PermissionModelMixin), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index ee664271..d53e706c 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -739,6 +739,33 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi indexes = (GinIndex(fields=["search_vector"]),) +class NutritionType(models.Model, PermissionModelMixin): + name = models.CharField(max_length=128) + unit = models.CharField(max_length=64, blank=True, null=True) + icon = models.CharField(max_length=16, blank=True, null=True) + description = models.CharField(max_length=512, blank=True, null=True) + + space = models.ForeignKey(Space, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + + def __str__(self): + return f'{self.name}' + + +class FoodNutrition(models.Model, PermissionModelMixin): + food_amount = models.DecimalField(default=0, decimal_places=2, max_digits=32) + food_unit = models.ForeignKey(Unit, on_delete=models.CASCADE) + food = models.ForeignKey(Food, on_delete=models.CASCADE) + nutrition_amount = models.DecimalField(default=0, decimal_places=4, max_digits=32) + nutrition_type = models.ForeignKey(NutritionType, on_delete=models.PROTECT) + + space = models.ForeignKey(Space, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + + def __str__(self): + return f'{self.food_amount} {self.food_unit} {self.food}: {self.nutrition_amount} {self.nutrition_type.unit} {self.nutrition_type.name}' + + class NutritionInformation(models.Model, PermissionModelMixin): fats = models.DecimalField(default=0, decimal_places=16, max_digits=32) carbohydrates = models.DecimalField( @@ -755,14 +782,6 @@ class NutritionInformation(models.Model, PermissionModelMixin): return f'Nutrition {self.pk}' -# class NutritionType(models.Model, PermissionModelMixin): -# name = models.CharField(max_length=128) -# icon = models.CharField(max_length=16, blank=True, null=True) -# description = models.CharField(max_length=512, blank=True, null=True) -# -# space = models.ForeignKey(Space, on_delete=models.CASCADE) -# objects = ScopedManager(space='space') - class RecipeManager(models.Manager.from_queryset(models.QuerySet)): def get_queryset(self): return super(RecipeManager, self).get_queryset().annotate(rating=Avg('cooklog__rating')).annotate(last_cooked=Max('cooklog__created_at')) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 3795150b..a4fde05d 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -29,7 +29,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu RecipeBookEntry, RecipeImport, ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, - SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion) + SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, FoodNutrition, NutritionType) from cookbook.templatetags.custom_tags import markdown from recipes.settings import AWS_ENABLED, MEDIA_URL @@ -619,6 +619,7 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer): used_in_recipes = serializers.SerializerMethodField('get_used_in_recipes') amount = CustomDecimalField() conversions = serializers.SerializerMethodField('get_conversions') + nutritions = serializers.SerializerMethodField('get_nutritions') def get_used_in_recipes(self, obj): return list(Recipe.objects.filter(steps__ingredients=obj.id).values('id', 'name')) @@ -629,7 +630,7 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer): for c in UnitConversion.objects.filter(space=self.context['request'].space).filter(Q(food__isnull=True) | Q(food=obj.food)).filter(Q(base_unit=obj.unit) | Q(converted_unit=obj.unit)): if obj.unit == c.base_unit: conversions.append({ - 'amount': obj.amount * (c.converted_amount/c.base_amount), + 'amount': obj.amount * (c.converted_amount / c.base_amount), 'unit': UnitSerializer(c.converted_unit, context={'request': self.context['request']}).data, }) else: @@ -640,6 +641,23 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer): return conversions + def get_nutritions(self, ingredient): + nutritions = {} + for nt in NutritionType.objects.filter(space=self.context['request'].space).all(): + nutritions[nt.id] = None + + food_nutrition = ingredient.food.foodnutrition_set.all() + for fn in food_nutrition: + if fn.food_unit == ingredient.unit: + nutritions[fn.nutrition_type.id] = ingredient.amount / fn.food_amount * fn.nutrition_amount + else: + conversions = self.get_conversions(ingredient) + for c in conversions: + if fn.food_unit.id == c['unit']['id']: + nutritions[fn.nutrition_type.id] = c['amount'] / fn.food_amount * fn.nutrition_amount + + return nutritions + def create(self, validated_data): validated_data['space'] = self.context['request'].space return super().create(validated_data) @@ -651,7 +669,7 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer): class Meta: model = Ingredient fields = ( - 'id', 'food', 'unit', 'amount', 'conversions', 'note', 'order', + 'id', 'food', 'unit', 'amount', 'nutritions', 'conversions', 'note', 'order', 'is_header', 'no_amount', 'original_text', 'used_in_recipes', 'always_use_plural_unit', 'always_use_plural_food', )