added recipe properties

This commit is contained in:
vabene1111 2023-05-06 19:14:25 +02:00
parent 763f71a05c
commit 60f31608b9
17 changed files with 1099 additions and 735 deletions

View File

@ -15,7 +15,7 @@ from .models import (BookmarkletImport, Comment, CookLog, Food, FoodInheritField
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, 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): class CustomUserAdmin(UserAdmin):
@ -331,7 +331,7 @@ class FoodPropertyTypeAdmin(admin.ModelAdmin):
list_display = ('id', 'name') list_display = ('id', 'name')
admin.site.register(FoodPropertyType, FoodPropertyTypeAdmin) admin.site.register(PropertyType, FoodPropertyTypeAdmin)
class FoodPropertyAdmin(admin.ModelAdmin): class FoodPropertyAdmin(admin.ModelAdmin):

View File

@ -1,6 +1,6 @@
from django.db.models import Q 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: class OpenDataImporter:
@ -55,14 +55,14 @@ class OpenDataImporter:
insert_list = [] insert_list = []
for k in list(self.data[datatype].keys()): for k in list(self.data[datatype].keys()):
insert_list.append(FoodPropertyType( insert_list.append(PropertyType(
name=self.data[datatype][k]['name'], name=self.data[datatype][k]['name'],
unit=self.data[datatype][k]['unit'], unit=self.data[datatype][k]['unit'],
open_data_slug=k, open_data_slug=k,
space=self.request.space 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): def import_supermarket(self):
datatype = 'supermarket' datatype = 'supermarket'
@ -114,7 +114,7 @@ class OpenDataImporter:
existing_objects[f[2]] = f existing_objects[f[2]] = f
self._update_slug_cache(Unit, 'unit') 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_unit_key = 'preferred_unit_metric'
pref_shopping_unit_key = 'preferred_packaging_unit_metric' pref_shopping_unit_key = 'preferred_packaging_unit_metric'

View File

@ -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: class FoodPropertyHelper:
@ -19,7 +19,7 @@ class FoodPropertyHelper:
""" """
ingredients = [] ingredients = []
computed_properties = {} 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(): for s in recipe.steps.all():
ingredients += s.ingredients.all() ingredients += s.ingredients.all()
@ -64,10 +64,10 @@ class FoodPropertyHelper:
food_1 = Food.objects.create(name='Food 1', space=self.space) food_1 = Food.objects.create(name='Food 1', space=self.space)
food_2 = Food.objects.create(name='Food 2', 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_fat = PropertyType.objects.create(name='Fat', unit='g', space=self.space)
property_calories = FoodPropertyType.objects.create(name='Calories', unit='kcal', space=self.space) property_calories = PropertyType.objects.create(name='Calories', unit='kcal', space=self.space)
property_nuts = FoodPropertyType.objects.create(name='Nuts', space=self.space) property_nuts = PropertyType.objects.create(name='Nuts', space=self.space)
property_price = FoodPropertyType.objects.create(name='Price', unit='', 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_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) 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 = 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) step_2.ingredients.create(amount=50, unit=unit_gram, food=food_1, space=self.space)
recipe_1.steps.add(step_2) 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

View File

@ -1,5 +1,6 @@
# import random # import random
import re import re
import traceback
from html import unescape from html import unescape
from django.core.cache import caches 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 import recipe_url_import as helper
from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.models import Automation, Keyword from cookbook.models import Automation, Keyword, PropertyType
# from unicodedata import decomposition # from unicodedata import decomposition
@ -193,6 +195,13 @@ def get_from_scraper(scrape, request):
except Exception: except Exception:
pass 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']: 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] 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: for a in automations:
@ -203,6 +212,30 @@ def get_from_scraper(scrape, request):
return recipe_json 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): def get_from_youtube_scraper(url, request):
"""A YouTube Information Scraper.""" """A YouTube Information Scraper."""
kw, created = Keyword.objects.get_or_create(name='YouTube', space=request.space) kw, created = Keyword.objects.get_or_create(name='YouTube', space=request.space)

View File

@ -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'),
),
]

View File

@ -755,7 +755,7 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
indexes = (GinIndex(fields=["search_vector"]),) indexes = (GinIndex(fields=["search_vector"]),)
class FoodPropertyType(models.Model, PermissionModelMixin): class PropertyType(models.Model, PermissionModelMixin):
NUTRITION = 'NUTRITION' NUTRITION = 'NUTRITION'
ALLERGEN = 'ALLERGEN' ALLERGEN = 'ALLERGEN'
PRICE = 'PRICE' PRICE = 'PRICE'
@ -780,7 +780,7 @@ class FoodPropertyType(models.Model, PermissionModelMixin):
class Meta: class Meta:
constraints = [ 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_unit = models.ForeignKey(Unit, on_delete=models.CASCADE)
food = models.ForeignKey(Food, 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_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) space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space') 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): class NutritionInformation(models.Model, PermissionModelMixin):
fats = models.DecimalField(default=0, decimal_places=16, max_digits=32) fats = models.DecimalField(default=0, decimal_places=16, max_digits=32)
carbohydrates = models.DecimalField( carbohydrates = models.DecimalField(
@ -841,6 +852,7 @@ class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModel
waiting_time = models.IntegerField(default=0) waiting_time = models.IntegerField(default=0)
internal = models.BooleanField(default=False) internal = models.BooleanField(default=False)
nutrition = models.ForeignKey(NutritionInformation, blank=True, null=True, on_delete=models.CASCADE) 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) show_ingredient_overview = models.BooleanField(default=True)
private = models.BooleanField(default=False) private = models.BooleanField(default=False)
shared = models.ManyToManyField(User, blank=True, related_name='recipe_shared_with') shared = models.ManyToManyField(User, blank=True, related_name='recipe_shared_with')

View File

@ -22,7 +22,7 @@ from rest_framework.exceptions import NotFound, ValidationError
from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
from cookbook.helper.HelperFunctions import str2bool 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.permission_helper import above_space_limit
from cookbook.helper.shopping_helper import RecipeShoppingEditor from cookbook.helper.shopping_helper import RecipeShoppingEditor
from cookbook.helper.unit_conversion_helper import UnitConversionHelper 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, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, FoodProperty, SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, FoodProperty,
FoodPropertyType) PropertyType, RecipeProperty)
from cookbook.templatetags.custom_tags import markdown from cookbook.templatetags.custom_tags import markdown
from recipes.settings import AWS_ENABLED, MEDIA_URL from recipes.settings import AWS_ENABLED, MEDIA_URL
@ -744,21 +744,21 @@ class UnitConversionSerializer(WritableNestedModelSerializer):
class Meta: class Meta:
model = UnitConversion 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): def create(self, validated_data):
validated_data['space'] = self.context['request'].space validated_data['space'] = self.context['request'].space
return super().create(validated_data) return super().create(validated_data)
class Meta: class Meta:
model = FoodPropertyType model = PropertyType
fields = ('id', 'name', 'icon', 'unit', 'description', 'open_data_slug') fields = ('id', 'name', 'icon', 'unit', 'description', 'open_data_slug')
class FoodPropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer): class FoodPropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
property_type = FoodPropertyTypeSerializer() property_type = PropertyTypeSerializer()
food = FoodSimpleSerializer() food = FoodSimpleSerializer()
food_unit = UnitSerializer() food_unit = UnitSerializer()
food_amount = CustomDecimalField() food_amount = CustomDecimalField()
@ -776,6 +776,20 @@ class FoodPropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
read_only_fields = ('id',) 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): class NutritionInformationSerializer(serializers.ModelSerializer):
carbohydrates = CustomDecimalField() carbohydrates = CustomDecimalField()
fats = CustomDecimalField() fats = CustomDecimalField()
@ -826,6 +840,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
class RecipeSerializer(RecipeBaseSerializer): class RecipeSerializer(RecipeBaseSerializer):
nutrition = NutritionInformationSerializer(allow_null=True, required=False) nutrition = NutritionInformationSerializer(allow_null=True, required=False)
properties = RecipePropertySerializer(many=True, required=False)
steps = StepSerializer(many=True) steps = StepSerializer(many=True)
keywords = KeywordSerializer(many=True) keywords = KeywordSerializer(many=True)
shared = UserSerializer(many=True, required=False) shared = UserSerializer(many=True, required=False)
@ -842,7 +857,7 @@ class RecipeSerializer(RecipeBaseSerializer):
fields = ( fields = (
'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time', 'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at', 'source_url', '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', 'last_cooked',
'private', 'shared', 'private', 'shared',
) )

View File

@ -270,7 +270,7 @@
</a> </a>
</div> </div>
<div class="col-4"> <div class="col-4">
<a href="{% url 'list_food_property_type' %}" class="p-0 p-md-1"> <a href="{% url 'list_property_type' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0"> <div class="card p-0 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters"> <div class="card-body text-center p-0 no-gutters">
<i class="fas fa-database fa-2x"></i> <i class="fas fa-database fa-2x"></i>

View File

@ -1,8 +1,8 @@
from django.contrib import auth from django.contrib import auth
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from cookbook.helper.food_property_helper import FoodPropertyHelper from cookbook.helper.property_helper import FoodPropertyHelper
from cookbook.models import Unit, Food, FoodPropertyType, FoodProperty, Recipe, Step from cookbook.models import Unit, Food, PropertyType, FoodProperty, Recipe, Step
def test_food_property(space_1, u1_s1): 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_1 = Food.objects.create(name='food_1', space=space_1)
food_2 = Food.objects.create(name='food_2', 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_fat = PropertyType.objects.create(name='property_fat', space=space_1)
property_calories = FoodPropertyType.objects.create(name='property_calories', space=space_1) property_calories = PropertyType.objects.create(name='property_calories', space=space_1)
property_nuts = FoodPropertyType.objects.create(name='property_nuts', space=space_1) property_nuts = PropertyType.objects.create(name='property_nuts', space=space_1)
property_price = FoodPropertyType.objects.create(name='property_price', 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_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) 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)

View File

@ -12,7 +12,7 @@ from recipes.version import VERSION_NUMBER
from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe, from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage, RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage,
Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UserFile, 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 import api, data, delete, edit, import_export, lists, new, telegram, views
from .views.api import CustomAuthToken, ImportOpenData 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: for m in vue_models:
py_name = get_model_name(m) py_name = get_model_name(m)
url_name = py_name.replace('_', '-') url_name = py_name.replace('_', '-')

View File

@ -70,7 +70,7 @@ from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilte
MealType, Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList, MealType, Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, 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.dropbox import Dropbox
from cookbook.provider.local import Local from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud from cookbook.provider.nextcloud import Nextcloud
@ -94,7 +94,7 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSeri
SyncLogSerializer, SyncSerializer, UnitSerializer, SyncLogSerializer, SyncSerializer, UnitSerializer,
UserFileSerializer, UserSerializer, UserPreferenceSerializer, UserFileSerializer, UserSerializer, UserPreferenceSerializer,
UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer, UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer,
RecipeExportSerializer, UnitConversionSerializer, FoodPropertyTypeSerializer, FoodPropertySerializer) RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer, FoodPropertySerializer)
from cookbook.views.import_export import get_integration from cookbook.views.import_export import get_integration
from recipes import settings 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 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 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', 'shared',
'properties',
'properties__property_type',
'steps',
'steps__ingredients', 'steps__ingredients',
'steps__ingredients__step_set', 'steps__ingredients__step_set',
'steps__ingredients__step_set__recipe_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_base_relation__base_unit',
'steps__ingredients__unit__unit_conversion_converted_relation', 'steps__ingredients__unit__unit_conversion_converted_relation',
'steps__ingredients__unit__unit_conversion_converted_relation__converted_unit', 'steps__ingredients__unit__unit_conversion_converted_relation__converted_unit',
'cooklog_set').select_related('nutrition')
'cooklog_set').select_related(
'nutrition')
return super().get_queryset() return super().get_queryset()
self.queryset = self.queryset.filter(space=self.request.space).filter( self.queryset = self.queryset.filter(space=self.request.space).filter(
@ -972,8 +975,8 @@ class UnitConversionViewSet(viewsets.ModelViewSet):
class FoodPropertyTypeViewSet(viewsets.ModelViewSet): class FoodPropertyTypeViewSet(viewsets.ModelViewSet):
queryset = FoodPropertyType.objects queryset = PropertyType.objects
serializer_class = FoodPropertyTypeSerializer serializer_class = PropertyTypeSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
def get_queryset(self): def get_queryset(self):

View File

@ -246,15 +246,15 @@ def unit_conversion(request):
@group_required('user') @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 # model-name is the models.js name of the model, probably ALL-CAPS
return render( return render(
request, request,
'generic/model_template.html', 'generic/model_template.html',
{ {
"title": _("Food Property Types"), "title": _("Property Types"),
"config": { "config": {
'model': "FOOD_PROPERTY_TYPE", # *REQUIRED* name of the model in models.js 'model': "PROPERTY_TYPE", # *REQUIRED* name of the model in models.js
} }
} }
) )

View File

@ -23,7 +23,7 @@ from oauth2_provider.models import AccessToken
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, ShoppingPreferenceForm, from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, ShoppingPreferenceForm,
SpaceCreateForm, SpaceJoinForm, User, SpaceCreateForm, SpaceJoinForm, User,
UserCreateForm, UserNameForm, UserPreference, UserPreferenceForm) 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.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, from cookbook.models import (Comment, CookLog, InviteLink, SearchFields, SearchPreference, ShareLink,
Space, ViewLog, UserSpace) Space, ViewLog, UserSpace)

View File

@ -31,7 +31,7 @@
</div> </div>
</div> </div>
<!-- Image and misc properties --> <!-- Image and misc -->
<div class="row pt-2"> <div class="row pt-2">
<div class="col-md-6" style="max-height: 50vh; min-height: 30vh"> <div class="col-md-6" style="max-height: 50vh; min-height: 30vh">
<input id="id_file_upload" ref="file_upload" type="file" hidden <input id="id_file_upload" ref="file_upload" type="file" hidden
@ -99,65 +99,53 @@
</div> </div>
</div> </div>
<!-- Nutrition -->
<div class="row pt-2"> <div class="row pt-2">
<div class="col-md-12"> <div class="col-md-12">
<div class="card border-grey"> <div class="card mt-2 mb-2">
<div class="card-header" style="display: table"> <div class="card-body pr-2 pl-2 pr-md-5 pl-md-5 pt-3 pb-3">
<div class="row"> <h6>{{ $t('Properties') }} <small class="text-muted"> {{$t('per_serving')}}</small></h6>
<div class="col-md-9 d-table">
<h5 class="d-table-cell align-middle">{{ $t("Nutrition") }}</h5> <div class="alert alert-info" role="alert">
{{ $t('recipe_property_info')}}
</div>
<div class="d-flex mt-2" v-for="p in recipe.properties" v-bind:key="p.id">
<div class="flex-fill w-50">
<generic-multiselect
@change="p.property_type = $event.val"
:initial_single_selection="p.property_type"
:label="'name'"
:model="Models.PROPERTY_TYPE"
:limit="25"
:multiple="false"
></generic-multiselect>
</div>
<div class="flex-fill w-50">
<div class="input-group">
<input type="number" class="form-control" v-model="p.property_amount">
<div class="input-group-append">
<span class="input-group-text" v-if="p.property_type !== null && p.property_type.unit !== ''">{{ p.property_type.unit }}</span>
<button class="btn btn-danger" @click="deleteProperty(p)"><i class="fa fa-trash fa-fw"></i></button>
</div>
</div>
</div>
</div>
<div class="flex-row mt-2">
<div class="flex-column w-25 offset-4">
<button class="btn btn-success btn-block" @click="addProperty()"><i class="fa fa-plus"></i></button>
</div>
</div>
</div> </div>
<div class="col-md-3">
<button
type="button"
@click="addNutrition()"
v-if="recipe.nutrition === null"
v-b-tooltip.hover
v-bind:title="$t('Add_nutrition_recipe')"
class="btn btn-sm btn-success shadow-none float-right"
>
<i class="fas fa-plus-circle"></i>
</button>
<button
type="button"
@click="removeNutrition()"
v-if="recipe.nutrition !== null"
v-b-tooltip.hover
v-bind:title="$t('Remove_nutrition_recipe')"
class="btn btn-sm btn-danger shadow-none float-right"
>
<i class="fas fa-trash-alt"></i>
</button>
</div> </div>
</div> </div>
</div> </div>
<b-collapse id="id_nutrition_collapse" class="mt-2" v-model="nutrition_visible"> <div class="row pt-2">
<div class="card-body" v-if="recipe.nutrition !== null"> <div class="col-md-12">
<b-alert show>
There is currently only very basic support for tracking nutritional information. A
<a href="https://github.com/vabene1111/recipes/issues/896" target="_blank"
rel="noreferrer nofollow">big update</a> is planned to improve on this in many
different areas.
</b-alert>
<label for="id_name"> {{ $t(energy()) }}</label>
<input class="form-control" id="id_calories" v-model="recipe.nutrition.calories"/>
<label for="id_name"> {{ $t("Carbohydrates") }}</label>
<input class="form-control" id="id_carbohydrates"
v-model="recipe.nutrition.carbohydrates"/>
<label for="id_name"> {{ $t("Fats") }}</label>
<input class="form-control" id="id_fats" v-model="recipe.nutrition.fats"/>
<label for="id_name"> {{ $t("Proteins") }}</label>
<input class="form-control" id="id_proteins" v-model="recipe.nutrition.proteins"/>
</div>
</b-collapse>
</div>
<b-card-header header-tag="header" class="p-1" role="tab"> <b-card-header header-tag="header" class="p-1" role="tab">
<b-button squared block v-b-toggle.additional_collapse class="text-left" <b-button squared block v-b-toggle.additional_collapse class="text-left"
variant="outline-primary">{{ $t("additional_options") }} variant="outline-primary">{{ $t("additional_options") }}
@ -1121,6 +1109,14 @@ export default {
let new_keyword = {label: tag, name: tag} let new_keyword = {label: tag, name: tag}
this.recipe.keywords.push(new_keyword) this.recipe.keywords.push(new_keyword)
}, },
addProperty: function () {
this.recipe.properties.push(
{'property_amount': 0, 'property_type': null}
)
},
deleteProperty: function (recipe_property) {
this.recipe.properties = this.recipe.properties.filter(p => p.id !== recipe_property.id)
},
searchKeywords: function (query) { searchKeywords: function (query) {
let apiFactory = new ApiApiFactory() let apiFactory = new ApiApiFactory()

View File

@ -14,6 +14,7 @@
"success_moving_resource": "Successfully moved a resource!", "success_moving_resource": "Successfully moved a resource!",
"success_merging_resource": "Successfully merged a resource!", "success_merging_resource": "Successfully merged a resource!",
"file_upload_disabled": "File upload is not enabled for your space.", "file_upload_disabled": "File upload is not enabled for your space.",
"recipe_property_info": "You can also add properties to foods to calculate them automatically based on your recipe!",
"warning_space_delete": "You can delete your space including all recipes, shopping lists, meal plans and whatever else you have created. This cannot be undone! Are you sure you want to do this ?", "warning_space_delete": "You can delete your space including all recipes, shopping lists, meal plans and whatever else you have created. This cannot be undone! Are you sure you want to do this ?",
"food_inherit_info": "Fields on food that should be inherited by default.", "food_inherit_info": "Fields on food that should be inherited by default.",
"facet_count_info": "Show recipe counts on search filters.", "facet_count_info": "Show recipe counts on search filters.",
@ -37,6 +38,7 @@
"Add_nutrition_recipe": "Add nutrition to recipe", "Add_nutrition_recipe": "Add nutrition to recipe",
"Remove_nutrition_recipe": "Delete nutrition from recipe", "Remove_nutrition_recipe": "Delete nutrition from recipe",
"Copy_template_reference": "Copy template reference", "Copy_template_reference": "Copy template reference",
"per_serving": "per servings",
"Save_and_View": "Save & View", "Save_and_View": "Save & View",
"Manage_Books": "Manage Books", "Manage_Books": "Manage Books",
"Meal_Plan": "Meal Plan", "Meal_Plan": "Meal Plan",

View File

@ -659,9 +659,9 @@ export class Models {
}, },
} }
static FOOD_PROPERTY_TYPE = { static PROPERTY_TYPE = {
name: "Food Property Type", name: "Property Type",
apiName: "FoodPropertyType", apiName: "PropertyType",
paginated: false, paginated: false,
list: { list: {
header_component: { header_component: {

File diff suppressed because it is too large Load Diff