From 38010117e56aa68136a7ef52c5f201cadd076387 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Sun, 26 Feb 2023 08:22:07 +0100 Subject: [PATCH] optimized unit conversion queries using filter breaks prefetch related --- ...alter_unitconversion_base_unit_and_more.py | 24 +++++ ...alter_unitconversion_base_unit_and_more.py | 24 +++++ cookbook/models.py | 4 +- cookbook/serializer.py | 99 +++++++++++-------- cookbook/views/api.py | 7 ++ 5 files changed, 113 insertions(+), 45 deletions(-) create mode 100644 cookbook/migrations/0192_alter_unitconversion_base_unit_and_more.py create mode 100644 cookbook/migrations/0193_alter_unitconversion_base_unit_and_more.py diff --git a/cookbook/migrations/0192_alter_unitconversion_base_unit_and_more.py b/cookbook/migrations/0192_alter_unitconversion_base_unit_and_more.py new file mode 100644 index 000000000..41da9e56e --- /dev/null +++ b/cookbook/migrations/0192_alter_unitconversion_base_unit_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.7 on 2023-02-26 06:30 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0191_foodnutrition_food_nutrition_unique_per_space_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='unitconversion', + name='base_unit', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unit_conversion_base_unit', to='cookbook.unit'), + ), + migrations.AlterField( + model_name='unitconversion', + name='converted_unit', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unit_conversion_converted_unit', to='cookbook.unit'), + ), + ] diff --git a/cookbook/migrations/0193_alter_unitconversion_base_unit_and_more.py b/cookbook/migrations/0193_alter_unitconversion_base_unit_and_more.py new file mode 100644 index 000000000..8037f98f5 --- /dev/null +++ b/cookbook/migrations/0193_alter_unitconversion_base_unit_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.7 on 2023-02-26 07:03 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0192_alter_unitconversion_base_unit_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='unitconversion', + name='base_unit', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unit_conversion_base_relation', to='cookbook.unit'), + ), + migrations.AlterField( + model_name='unitconversion', + name='converted_unit', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unit_conversion_converted_relation', to='cookbook.unit'), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index 2fa06145a..76ff8ad73 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -652,9 +652,9 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin): class UnitConversion(ExportModelOperationsMixin('unit_conversion'), models.Model, PermissionModelMixin): base_amount = models.DecimalField(default=0, decimal_places=16, max_digits=32) - base_unit = models.ForeignKey('Unit', on_delete=models.CASCADE, related_name='base_unit') + base_unit = models.ForeignKey('Unit', on_delete=models.CASCADE, related_name='unit_conversion_base_relation') converted_amount = models.DecimalField(default=0, decimal_places=16, max_digits=32) - converted_unit = models.ForeignKey('Unit', on_delete=models.CASCADE, related_name='converted_unit') + converted_unit = models.ForeignKey('Unit', on_delete=models.CASCADE, related_name='unit_conversion_converted_relation') food = models.ForeignKey('Food', on_delete=models.CASCADE, null=True, blank=True) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index f770623c2..f6ebf49a6 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -29,7 +29,8 @@ 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, FoodNutrition, NutritionType) + SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, FoodNutrition, + NutritionType) from cookbook.templatetags.custom_tags import markdown from recipes.settings import AWS_ENABLED, MEDIA_URL @@ -276,10 +277,12 @@ class SpaceSerializer(WritableNestedModelSerializer): class Meta: model = Space - fields = ('id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users', - 'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb', - 'image', 'use_plural',) - read_only_fields = ('id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo',) + fields = ( + 'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users', + 'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb', + 'image', 'use_plural',) + read_only_fields = ( + 'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo',) class UserSpaceSerializer(WritableNestedModelSerializer): @@ -440,7 +443,8 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin): return unit space = validated_data.pop('space', self.context['request'].space) - obj, created = Unit.objects.get_or_create(name=name, plural_name=plural_name, space=space, defaults=validated_data) + obj, created = Unit.objects.get_or_create(name=name, plural_name=plural_name, space=space, + defaults=validated_data) return obj def update(self, instance, validated_data): @@ -579,7 +583,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR else: validated_data['onhand_users'] = list(set(onhand_users) - set(shared_users)) - obj, created = Food.objects.get_or_create(name=name, plural_name=plural_name, space=space, defaults=validated_data) + obj, created = Food.objects.get_or_create(name=name, plural_name=plural_name, space=space, + defaults=validated_data) return obj def update(self, instance, validated_data): @@ -626,35 +631,35 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer): def get_conversions(self, obj): conversions = [] - - 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), - 'unit': UnitSerializer(c.converted_unit, context={'request': self.context['request']}).data, - }) - else: - conversions.append({ - 'amount': obj.amount * (c.base_amount / c.converted_amount), - 'unit': UnitSerializer(c.base_unit, context={'request': self.context['request']}).data, - }) + # TODO add hardcoded base conversions for metric/imperial + if obj.unit: # TODO move to function and also iterate obj.unit.unit_conversion_converted_relation.all() + for c in obj.unit.unit_conversion_base_relation.all(): + if c.food is None or c.food == obj.food: + if obj.unit == c.base_unit: + conversions.append({ + 'amount': obj.amount * (c.converted_amount / c.base_amount), + 'unit': UnitSerializer(c.converted_unit, context={'request': self.context['request']}).data, + }) + else: + conversions.append({ + 'amount': obj.amount * (c.base_amount / c.converted_amount), + 'unit': UnitSerializer(c.base_unit, context={'request': self.context['request']}).data, + }) 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 + if ingredient.food: + for fn in ingredient.food.foodnutrition_set.all(): + 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 @@ -812,7 +817,8 @@ class RecipeSerializer(RecipeBaseSerializer): fields = ( 'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time', 'waiting_time', 'created_by', 'created_at', 'updated_at', 'source_url', - 'internal', 'show_ingredient_overview', 'nutrition', 'servings', 'file_path', 'servings_text', 'rating', 'last_cooked', + 'internal', 'show_ingredient_overview', 'nutrition', 'servings', 'file_path', 'servings_text', 'rating', + 'last_cooked', 'private', 'shared', ) read_only_fields = ['image', 'created_by', 'created_at'] @@ -947,11 +953,11 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer): value = value.quantize( Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero return ( - obj.name - or getattr(obj.mealplan, 'title', None) - or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)]) - or obj.recipe.name - ) + f' ({value:.2g})' + obj.name + or getattr(obj.mealplan, 'title', None) + or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)]) + or obj.recipe.name + ) + f' ({value:.2g})' def update(self, instance, validated_data): # TODO remove once old shopping list @@ -1152,13 +1158,19 @@ class InviteLinkSerializer(WritableNestedModelSerializer): if obj.email: try: - if InviteLink.objects.filter(space=self.context['request'].space, created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20: - message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(self.context['request'].user.get_user_display_name()) - message += _(' to join their Tandoor Recipes space ') + escape(self.context['request'].space.name) + '.\n\n' - message += _('Click the following link to activate your account: ') + self.context['request'].build_absolute_uri(reverse('view_invite', args=[str(obj.uuid)])) + '\n\n' - message += _('If the link does not work use the following code to manually join the space: ') + str(obj.uuid) + '\n\n' + if InviteLink.objects.filter(space=self.context['request'].space, + created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20: + message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape( + self.context['request'].user.get_user_display_name()) + message += _(' to join their Tandoor Recipes space ') + escape( + self.context['request'].space.name) + '.\n\n' + message += _('Click the following link to activate your account: ') + self.context[ + 'request'].build_absolute_uri(reverse('view_invite', args=[str(obj.uuid)])) + '\n\n' + message += _('If the link does not work use the following code to manually join the space: ') + str( + obj.uuid) + '\n\n' message += _('The invitation is valid until ') + str(obj.valid_until) + '\n\n' - message += _('Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub ') + 'https://github.com/vabene1111/recipes/' + message += _( + 'Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub ') + 'https://github.com/vabene1111/recipes/' send_mail( _('Tandoor Recipes Invite'), @@ -1267,7 +1279,8 @@ class IngredientExportSerializer(WritableNestedModelSerializer): class Meta: model = Ingredient - fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount', 'always_use_plural_unit', 'always_use_plural_food') + fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount', 'always_use_plural_unit', + 'always_use_plural_food') class StepExportSerializer(WritableNestedModelSerializer): diff --git a/cookbook/views/api.py b/cookbook/views/api.py index f8917aa4a..913689fa5 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -805,7 +805,14 @@ class RecipeViewSet(viewsets.ModelViewSet): 'steps__ingredients__food__substitute', 'steps__ingredients__food__child_inherit_fields', 'steps__ingredients__food__foodnutrition_set', + 'steps__ingredients__food__foodnutrition_set__food', + 'steps__ingredients__food__foodnutrition_set__food_unit', + 'steps__ingredients__food__foodnutrition_set__nutrition_type', 'steps__ingredients__unit', + 'steps__ingredients__unit__unit_conversion_base_relation', + 'steps__ingredients__unit__unit_conversion_base_relation__base_unit', + 'steps__ingredients__unit__unit_conversion_converted_relation', + 'steps__ingredients__unit__unit_conversion_converted_relation__converted_unit', 'cooklog_set').select_related( 'nutrition')