diff --git a/cookbook/admin.py b/cookbook/admin.py index cd0acf9e..32595592 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, FoodPropertyType, FoodProperty) + TelegramBot, Unit, UserFile, UserPreference, ViewLog, Automation, UserSpace, UnitConversion, PropertyType, FoodProperty) class CustomUserAdmin(UserAdmin): @@ -331,7 +331,7 @@ class FoodPropertyTypeAdmin(admin.ModelAdmin): list_display = ('id', 'name') -admin.site.register(FoodPropertyType, FoodPropertyTypeAdmin) +admin.site.register(PropertyType, FoodPropertyTypeAdmin) class FoodPropertyAdmin(admin.ModelAdmin): diff --git a/cookbook/helper/open_data_importer.py b/cookbook/helper/open_data_importer.py index 938c2c76..0bd3ac51 100644 --- a/cookbook/helper/open_data_importer.py +++ b/cookbook/helper/open_data_importer.py @@ -1,6 +1,6 @@ from django.db.models import Q -from cookbook.models import Unit, SupermarketCategory, FoodProperty, FoodPropertyType, Supermarket, SupermarketCategoryRelation, Food, Automation, UnitConversion +from cookbook.models import Unit, SupermarketCategory, FoodProperty, PropertyType, Supermarket, SupermarketCategoryRelation, Food, Automation, UnitConversion class OpenDataImporter: @@ -55,14 +55,14 @@ class OpenDataImporter: insert_list = [] for k in list(self.data[datatype].keys()): - insert_list.append(FoodPropertyType( + insert_list.append(PropertyType( name=self.data[datatype][k]['name'], unit=self.data[datatype][k]['unit'], open_data_slug=k, space=self.request.space )) - return FoodPropertyType.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('open_data_slug',), unique_fields=('space', 'name',)) + return PropertyType.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('open_data_slug',), unique_fields=('space', 'name',)) def import_supermarket(self): datatype = 'supermarket' @@ -114,7 +114,7 @@ class OpenDataImporter: existing_objects[f[2]] = f self._update_slug_cache(Unit, 'unit') - self._update_slug_cache(FoodPropertyType, 'property') + self._update_slug_cache(PropertyType, 'property') pref_unit_key = 'preferred_unit_metric' pref_shopping_unit_key = 'preferred_packaging_unit_metric' diff --git a/cookbook/helper/food_property_helper.py b/cookbook/helper/property_helper.py similarity index 84% rename from cookbook/helper/food_property_helper.py rename to cookbook/helper/property_helper.py index d1aa5575..8c8d8596 100644 --- a/cookbook/helper/food_property_helper.py +++ b/cookbook/helper/property_helper.py @@ -1,4 +1,4 @@ -from cookbook.models import FoodPropertyType, Unit, Food, FoodProperty, Recipe, Step +from cookbook.models import PropertyType, Unit, Food, FoodProperty, Recipe, Step class FoodPropertyHelper: @@ -19,7 +19,7 @@ class FoodPropertyHelper: """ ingredients = [] computed_properties = {} - property_types = FoodPropertyType.objects.filter(space=self.space).all() + property_types = PropertyType.objects.filter(space=self.space).all() for s in recipe.steps.all(): ingredients += s.ingredients.all() @@ -64,10 +64,10 @@ class FoodPropertyHelper: food_1 = Food.objects.create(name='Food 1', space=self.space) food_2 = Food.objects.create(name='Food 2', space=self.space) - property_fat = FoodPropertyType.objects.create(name='Fat', unit='g', space=self.space) - property_calories = FoodPropertyType.objects.create(name='Calories', unit='kcal', space=self.space) - property_nuts = FoodPropertyType.objects.create(name='Nuts', space=self.space) - property_price = FoodPropertyType.objects.create(name='Price', unit='€', space=self.space) + property_fat = PropertyType.objects.create(name='Fat', unit='g', space=self.space) + property_calories = PropertyType.objects.create(name='Calories', unit='kcal', space=self.space) + property_nuts = PropertyType.objects.create(name='Nuts', space=self.space) + property_price = PropertyType.objects.create(name='Price', unit='€', space=self.space) food_1_property_fat = FoodProperty.objects.create(food_amount=100, food_unit=unit_gram, food=food_1, property_amount=50, property_type=property_fat, space=self.space) food_1_property_nuts = FoodProperty.objects.create(food_amount=100, food_unit=unit_gram, food=food_1, property_amount=1, property_type=property_nuts, space=self.space) @@ -87,3 +87,18 @@ class FoodPropertyHelper: step_2 = Step.objects.create(instruction='instruction_step_1', space=self.space) step_2.ingredients.create(amount=50, unit=unit_gram, food=food_1, space=self.space) recipe_1.steps.add(step_2) + + +class RecipePropertyHelper: + space = None + + def __init__(self, space): + """ + Helper to perform recipe property operations + :param space: space to limit scope to + """ + self.space = space + + + def parse_properties_from_schema(self, schema): + pass \ No newline at end of file diff --git a/cookbook/helper/recipe_url_import.py b/cookbook/helper/recipe_url_import.py index f9398294..790b7490 100644 --- a/cookbook/helper/recipe_url_import.py +++ b/cookbook/helper/recipe_url_import.py @@ -1,5 +1,6 @@ # import random import re +import traceback from html import unescape from django.core.cache import caches @@ -12,7 +13,8 @@ from recipe_scrapers._utils import get_host_name, get_minutes # from cookbook.helper import recipe_url_import as helper from cookbook.helper.ingredient_parser import IngredientParser -from cookbook.models import Automation, Keyword +from cookbook.models import Automation, Keyword, PropertyType + # from unicodedata import decomposition @@ -193,6 +195,13 @@ def get_from_scraper(scrape, request): except Exception: pass + try: + recipe_json['properties'] = get_recipe_properties(request.space, scrape.schema.nutrients()) + print(recipe_json['properties']) + except Exception: + traceback.print_exc() + pass + if recipe_json['source_url']: automations = Automation.objects.filter(type=Automation.INSTRUCTION_REPLACE, space=request.space, disabled=False).only('param_1', 'param_2', 'param_3').order_by('order').all()[:512] for a in automations: @@ -203,6 +212,30 @@ def get_from_scraper(scrape, request): return recipe_json +def get_recipe_properties(space, property_data): + # {'servingSize': '1', 'calories': '302 kcal', 'proteinContent': '7,66g', 'fatContent': '11,56g', 'carbohydrateContent': '41,33g'} + properties = { + "property-calories": "calories", + "property-carbohydrates": "carbohydrateContent", + "property-proteins": "proteinContent", + "property-fats": "fatContent", + } + recipe_properties = [] + for pt in PropertyType.objects.filter(space=space, open_data_slug__in=list(properties.keys())).all(): + for p in list(properties.keys()): + if pt.open_data_slug == p: + if properties[p] in property_data: + recipe_properties.append({ + 'property_type': { + 'id': pt.id, + 'name': pt.name, + }, + 'property_amount': parse_servings(property_data[properties[p]]) / float(property_data['servingSize']), + }) + + return recipe_properties + + def get_from_youtube_scraper(url, request): """A YouTube Information Scraper.""" kw, created = Keyword.objects.get_or_create(name='YouTube', space=request.space) diff --git a/cookbook/migrations/0195_rename_foodpropertytype_propertytype_recipeproperty_and_more.py b/cookbook/migrations/0195_rename_foodpropertytype_propertytype_recipeproperty_and_more.py new file mode 100644 index 00000000..150accad --- /dev/null +++ b/cookbook/migrations/0195_rename_foodpropertytype_propertytype_recipeproperty_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 4.1.7 on 2023-05-06 16:33 + +import cookbook.models +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0194_supermarketcategoryrelation_unique_sm_category_relation'), + ] + + operations = [ + migrations.RenameModel( + old_name='FoodPropertyType', + new_name='PropertyType', + ), + migrations.CreateModel( + name='RecipeProperty', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('property_amount', models.DecimalField(decimal_places=4, default=0, max_digits=32)), + ('property_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cookbook.propertytype')), + ('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')), + ], + bases=(models.Model, cookbook.models.PermissionModelMixin), + ), + migrations.AddField( + model_name='recipe', + name='properties', + field=models.ManyToManyField(blank=True, to='cookbook.recipeproperty'), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index 382b6d0d..7a7891d0 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -755,7 +755,7 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi indexes = (GinIndex(fields=["search_vector"]),) -class FoodPropertyType(models.Model, PermissionModelMixin): +class PropertyType(models.Model, PermissionModelMixin): NUTRITION = 'NUTRITION' ALLERGEN = 'ALLERGEN' PRICE = 'PRICE' @@ -780,7 +780,7 @@ class FoodPropertyType(models.Model, PermissionModelMixin): class Meta: constraints = [ - models.UniqueConstraint(fields=['space', 'name'], name='food_property_type_unique_name_per_space') + models.UniqueConstraint(fields=['space', 'name'], name='property_type_unique_name_per_space') ] @@ -789,7 +789,7 @@ class FoodProperty(models.Model, PermissionModelMixin): food_unit = models.ForeignKey(Unit, on_delete=models.CASCADE) food = models.ForeignKey(Food, on_delete=models.CASCADE) property_amount = models.DecimalField(default=0, decimal_places=4, max_digits=32) - property_type = models.ForeignKey(FoodPropertyType, on_delete=models.PROTECT) + property_type = models.ForeignKey(PropertyType, on_delete=models.PROTECT) space = models.ForeignKey(Space, on_delete=models.CASCADE) objects = ScopedManager(space='space') @@ -803,6 +803,17 @@ class FoodProperty(models.Model, PermissionModelMixin): ] +class RecipeProperty(models.Model, PermissionModelMixin): + property_amount = models.DecimalField(default=0, decimal_places=4, max_digits=32) + property_type = models.ForeignKey(PropertyType, on_delete=models.PROTECT) + + space = models.ForeignKey(Space, on_delete=models.CASCADE) + objects = ScopedManager(space='space') + + def __str__(self): + return f'{self.property_amount} {self.property_type.unit} {self.property_type.name}' + + class NutritionInformation(models.Model, PermissionModelMixin): fats = models.DecimalField(default=0, decimal_places=16, max_digits=32) carbohydrates = models.DecimalField( @@ -841,6 +852,7 @@ class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModel waiting_time = models.IntegerField(default=0) internal = models.BooleanField(default=False) nutrition = models.ForeignKey(NutritionInformation, blank=True, null=True, on_delete=models.CASCADE) + properties = models.ManyToManyField(RecipeProperty, blank=True) show_ingredient_overview = models.BooleanField(default=True) private = models.BooleanField(default=False) shared = models.ManyToManyField(User, blank=True, related_name='recipe_shared_with') diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 589a077d..08133f6f 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -22,7 +22,7 @@ from rest_framework.exceptions import NotFound, ValidationError from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage from cookbook.helper.HelperFunctions import str2bool -from cookbook.helper.food_property_helper import FoodPropertyHelper +from cookbook.helper.property_helper import FoodPropertyHelper from cookbook.helper.permission_helper import above_space_limit from cookbook.helper.shopping_helper import RecipeShoppingEditor from cookbook.helper.unit_conversion_helper import UnitConversionHelper @@ -33,7 +33,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, FoodProperty, - FoodPropertyType) + PropertyType, RecipeProperty) from cookbook.templatetags.custom_tags import markdown from recipes.settings import AWS_ENABLED, MEDIA_URL @@ -744,21 +744,21 @@ class UnitConversionSerializer(WritableNestedModelSerializer): class Meta: model = UnitConversion - fields = ('id', 'name','base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'food', 'open_data_slug') + fields = ('id', 'name', 'base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'food', 'open_data_slug') -class FoodPropertyTypeSerializer(serializers.ModelSerializer): +class PropertyTypeSerializer(serializers.ModelSerializer): def create(self, validated_data): validated_data['space'] = self.context['request'].space return super().create(validated_data) class Meta: - model = FoodPropertyType + model = PropertyType fields = ('id', 'name', 'icon', 'unit', 'description', 'open_data_slug') class FoodPropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer): - property_type = FoodPropertyTypeSerializer() + property_type = PropertyTypeSerializer() food = FoodSimpleSerializer() food_unit = UnitSerializer() food_amount = CustomDecimalField() @@ -776,6 +776,20 @@ class FoodPropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer): read_only_fields = ('id',) +class RecipePropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer): + property_type = PropertyTypeSerializer() + property_amount = CustomDecimalField() + + def create(self, validated_data): + validated_data['space'] = self.context['request'].space + return super().create(validated_data) + + class Meta: + model = RecipeProperty + fields = ('id', 'property_type', 'property_amount',) + read_only_fields = ('id',) + + class NutritionInformationSerializer(serializers.ModelSerializer): carbohydrates = CustomDecimalField() fats = CustomDecimalField() @@ -826,6 +840,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer): class RecipeSerializer(RecipeBaseSerializer): nutrition = NutritionInformationSerializer(allow_null=True, required=False) + properties = RecipePropertySerializer(many=True, required=False) steps = StepSerializer(many=True) keywords = KeywordSerializer(many=True) shared = UserSerializer(many=True, required=False) @@ -842,7 +857,7 @@ 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', 'food_properties', 'servings', 'file_path', 'servings_text', 'rating', + 'internal', 'show_ingredient_overview', 'nutrition', 'properties', 'food_properties', 'servings', 'file_path', 'servings_text', 'rating', 'last_cooked', 'private', 'shared', ) diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html index 59c17420..a70e4294 100644 --- a/cookbook/templates/base.html +++ b/cookbook/templates/base.html @@ -270,7 +270,7 @@
- +
diff --git a/cookbook/tests/other/test_food_property.py b/cookbook/tests/other/test_food_property.py index a9345957..c223ee39 100644 --- a/cookbook/tests/other/test_food_property.py +++ b/cookbook/tests/other/test_food_property.py @@ -1,8 +1,8 @@ from django.contrib import auth from django_scopes import scopes_disabled -from cookbook.helper.food_property_helper import FoodPropertyHelper -from cookbook.models import Unit, Food, FoodPropertyType, FoodProperty, Recipe, Step +from cookbook.helper.property_helper import FoodPropertyHelper +from cookbook.models import Unit, Food, PropertyType, FoodProperty, Recipe, Step def test_food_property(space_1, u1_s1): @@ -16,10 +16,10 @@ def test_food_property(space_1, u1_s1): food_1 = Food.objects.create(name='food_1', space=space_1) food_2 = Food.objects.create(name='food_2', space=space_1) - property_fat = FoodPropertyType.objects.create(name='property_fat', space=space_1) - property_calories = FoodPropertyType.objects.create(name='property_calories', space=space_1) - property_nuts = FoodPropertyType.objects.create(name='property_nuts', space=space_1) - property_price = FoodPropertyType.objects.create(name='property_price', space=space_1) + property_fat = PropertyType.objects.create(name='property_fat', space=space_1) + property_calories = PropertyType.objects.create(name='property_calories', space=space_1) + property_nuts = PropertyType.objects.create(name='property_nuts', space=space_1) + property_price = PropertyType.objects.create(name='property_price', space=space_1) food_1_property_fat = FoodProperty.objects.create(food_amount=100, food_unit=unit_gram, food=food_1, property_amount=50, property_type=property_fat, space=space_1) food_1_property_nuts = FoodProperty.objects.create(food_amount=100, food_unit=unit_gram, food=food_1, property_amount=1, property_type=property_nuts, space=space_1) diff --git a/cookbook/urls.py b/cookbook/urls.py index 74e529cf..07d21005 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -12,7 +12,7 @@ from recipes.version import VERSION_NUMBER from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UserFile, - get_model_name, UserSpace, Space, FoodPropertyType, UnitConversion) + get_model_name, UserSpace, Space, PropertyType, UnitConversion) from .views import api, data, delete, edit, import_export, lists, new, telegram, views from .views.api import CustomAuthToken, ImportOpenData @@ -193,7 +193,7 @@ for m in generic_models: ) ) -vue_models = [Food, Keyword, Unit, Supermarket, SupermarketCategory, Automation, UserFile, Step, CustomFilter, UnitConversion, FoodPropertyType] +vue_models = [Food, Keyword, Unit, Supermarket, SupermarketCategory, Automation, UserFile, Step, CustomFilter, UnitConversion, PropertyType] for m in vue_models: py_name = get_model_name(m) url_name = py_name.replace('_', '-') diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 2d6581a2..f5618987 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -70,7 +70,7 @@ from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilte MealType, Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, - SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, FoodPropertyType, FoodProperty) + SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, PropertyType, FoodProperty) from cookbook.provider.dropbox import Dropbox from cookbook.provider.local import Local from cookbook.provider.nextcloud import Nextcloud @@ -94,7 +94,7 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSeri SyncLogSerializer, SyncSerializer, UnitSerializer, UserFileSerializer, UserSerializer, UserPreferenceSerializer, UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer, - RecipeExportSerializer, UnitConversionSerializer, FoodPropertyTypeSerializer, FoodPropertySerializer) + RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer, FoodPropertySerializer) from cookbook.views.import_export import get_integration from recipes import settings @@ -811,8 +811,12 @@ class RecipeViewSet(viewsets.ModelViewSet): if self.detail: # if detail request and not list, private condition is verified by permission class if not share: # filter for space only if not shared - self.queryset = self.queryset.filter(space=self.request.space).prefetch_related('steps', 'keywords', + self.queryset = self.queryset.filter(space=self.request.space).prefetch_related( + 'keywords', 'shared', + 'properties', + 'properties__property_type', + 'steps', 'steps__ingredients', 'steps__ingredients__step_set', 'steps__ingredients__step_set__recipe_set', @@ -831,9 +835,8 @@ class RecipeViewSet(viewsets.ModelViewSet): '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') - 'cooklog_set').select_related( - 'nutrition') return super().get_queryset() self.queryset = self.queryset.filter(space=self.request.space).filter( @@ -972,8 +975,8 @@ class UnitConversionViewSet(viewsets.ModelViewSet): class FoodPropertyTypeViewSet(viewsets.ModelViewSet): - queryset = FoodPropertyType.objects - serializer_class = FoodPropertyTypeSerializer + queryset = PropertyType.objects + serializer_class = PropertyTypeSerializer permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] def get_queryset(self): diff --git a/cookbook/views/lists.py b/cookbook/views/lists.py index 10d07cac..8c24d8a5 100644 --- a/cookbook/views/lists.py +++ b/cookbook/views/lists.py @@ -246,15 +246,15 @@ def unit_conversion(request): @group_required('user') -def food_property_type(request): +def property_type(request): # model-name is the models.js name of the model, probably ALL-CAPS return render( request, 'generic/model_template.html', { - "title": _("Food Property Types"), + "title": _("Property Types"), "config": { - 'model': "FOOD_PROPERTY_TYPE", # *REQUIRED* name of the model in models.js + 'model': "PROPERTY_TYPE", # *REQUIRED* name of the model in models.js } } ) diff --git a/cookbook/views/views.py b/cookbook/views/views.py index c9b0d627..916369aa 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -23,7 +23,7 @@ from oauth2_provider.models import AccessToken from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, ShoppingPreferenceForm, SpaceCreateForm, SpaceJoinForm, User, UserCreateForm, UserNameForm, UserPreference, UserPreferenceForm) -from cookbook.helper.food_property_helper import FoodPropertyHelper +from cookbook.helper.property_helper import FoodPropertyHelper from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid, switch_user_active_space from cookbook.models import (Comment, CookLog, InviteLink, SearchFields, SearchPreference, ShareLink, Space, ViewLog, UserSpace) diff --git a/vue/src/apps/RecipeEditView/RecipeEditView.vue b/vue/src/apps/RecipeEditView/RecipeEditView.vue index b21cb0ab..74695842 100644 --- a/vue/src/apps/RecipeEditView/RecipeEditView.vue +++ b/vue/src/apps/RecipeEditView/RecipeEditView.vue @@ -31,18 +31,18 @@
- +
@@ -99,65 +99,53 @@
-