properties structure imporioved

This commit is contained in:
vabene1111 2023-05-25 16:13:16 +02:00
parent 2a538abf80
commit 3302dacdc3
13 changed files with 4721 additions and 790 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, PropertyType, FoodProperty, RecipeProperty) TelegramBot, Unit, UserFile, UserPreference, ViewLog, Automation, UserSpace, UnitConversion, PropertyType, Property)
class CustomUserAdmin(UserAdmin): class CustomUserAdmin(UserAdmin):
@ -334,18 +334,11 @@ class PropertyTypeAdmin(admin.ModelAdmin):
admin.site.register(PropertyType, PropertyTypeAdmin) admin.site.register(PropertyType, PropertyTypeAdmin)
class FoodPropertyAdmin(admin.ModelAdmin): class PropertyAdmin(admin.ModelAdmin):
list_display = ('id', 'food_amount', 'food_unit', 'food', 'property_amount', 'property_type') list_display = ('property_amount', 'property_type')
admin.site.register(FoodProperty, FoodPropertyAdmin) admin.site.register(Property, PropertyAdmin)
class RecipePropertyAdmin(admin.ModelAdmin):
list_display = ('id', 'property_type', 'property_amount',)
admin.site.register(RecipeProperty, RecipePropertyAdmin)
class NutritionInformationAdmin(admin.ModelAdmin): class NutritionInformationAdmin(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, PropertyType, Supermarket, SupermarketCategoryRelation, Food, Automation, UnitConversion from cookbook.models import Unit, SupermarketCategory, Property, PropertyType, Supermarket, SupermarketCategoryRelation, Food, Automation, UnitConversion
class OpenDataImporter: class OpenDataImporter:
@ -169,10 +169,7 @@ class OpenDataImporter:
alias_list = [] alias_list = []
for k in list(self.data[datatype].keys()): for k in list(self.data[datatype].keys()):
for fp in self.data[datatype][k]['properties']['type_values']: for fp in self.data[datatype][k]['properties']['type_values']:
food_property_list.append(FoodProperty( food_property_list.append(Property(
food_id=self.slug_id_cache['food'][k],
food_amount=self.data[datatype][k]['properties']['food_amount'],
food_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['properties']['food_unit']],
property_type_id=self.slug_id_cache['property'][fp['property_type']], property_type_id=self.slug_id_cache['property'][fp['property_type']],
property_amount=fp['property_value'], property_amount=fp['property_value'],
space=self.request.space, space=self.request.space,
@ -186,7 +183,7 @@ class OpenDataImporter:
created_by=self.request.user, created_by=self.request.user,
)) ))
FoodProperty.objects.bulk_create(food_property_list, ignore_conflicts=True, unique_fields=('space', 'food', 'property_type',)) Property.objects.bulk_create(food_property_list, ignore_conflicts=True, unique_fields=('space', 'food', 'property_type',))
Automation.objects.bulk_create(alias_list, ignore_conflicts=True, unique_fields=('space', 'param_1', 'param_2',)) Automation.objects.bulk_create(alias_list, ignore_conflicts=True, unique_fields=('space', 'param_1', 'param_2',))
return insert_list + update_list return insert_list + update_list

View File

@ -2,7 +2,7 @@ from django.core.cache import caches
from cookbook.helper.cache_helper import CacheHelper from cookbook.helper.cache_helper import CacheHelper
from cookbook.helper.unit_conversion_helper import UnitConversionHelper from cookbook.helper.unit_conversion_helper import UnitConversionHelper
from cookbook.models import PropertyType, Unit, Food, FoodProperty, Recipe, Step from cookbook.models import PropertyType, Unit, Food, Property, Recipe, Step
class FoodPropertyHelper: class FoodPropertyHelper:
@ -42,13 +42,13 @@ class FoodPropertyHelper:
conversions = uch.get_conversions(i) conversions = uch.get_conversions(i)
for pt in property_types: for pt in property_types:
found_property = False found_property = False
for p in i.food.foodproperty_set.all(): for p in i.food.properties.all():
if p.property_type == pt: if p.property_type == pt:
for c in conversions: for c in conversions:
if c.unit == p.food_unit: if c.unit == i.food.properties_food_unit:
found_property = True found_property = True
computed_properties[pt.id]['total_value'] += (c.amount / p.food_amount) * p.property_amount computed_properties[pt.id]['total_value'] += (c.amount / i.food.properties_food_amount) * p.property_amount
computed_properties[pt.id]['food_values'] = self.add_or_create(computed_properties[p.property_type.id]['food_values'], c.food.id, (c.amount / p.food_amount) * p.property_amount, c.food) computed_properties[pt.id]['food_values'] = self.add_or_create(computed_properties[p.property_type.id]['food_values'], c.food.id, (c.amount / i.food.properties_food_amount) * p.property_amount, c.food)
if not found_property: if not found_property:
computed_properties[pt.id]['missing_value'] = True computed_properties[pt.id]['missing_value'] = True
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': i.food.name, 'value': 0} computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': i.food.name, 'value': 0}

View File

@ -1,4 +1,4 @@
# Generated by Django 4.1.7 on 2023-05-06 19:16 # Generated by Django 4.1.9 on 2023-05-25 13:05
import cookbook.models import cookbook.models
from django.conf import settings from django.conf import settings
@ -16,10 +16,9 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='FoodProperty', name='Property',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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)),
('property_amount', models.DecimalField(decimal_places=4, default=0, max_digits=32)), ('property_amount', models.DecimalField(decimal_places=4, default=0, max_digits=32)),
], ],
bases=(models.Model, cookbook.models.PermissionModelMixin), bases=(models.Model, cookbook.models.PermissionModelMixin),
@ -37,14 +36,6 @@ class Migration(migrations.Migration):
], ],
bases=(models.Model, cookbook.models.PermissionModelMixin), bases=(models.Model, cookbook.models.PermissionModelMixin),
), ),
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)),
],
bases=(models.Model, cookbook.models.PermissionModelMixin),
),
migrations.CreateModel( migrations.CreateModel(
name='UnitConversion', name='UnitConversion',
fields=[ fields=[
@ -77,6 +68,16 @@ class Migration(migrations.Migration):
name='preferred_unit', name='preferred_unit',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='preferred_unit', to='cookbook.unit'), field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='preferred_unit', to='cookbook.unit'),
), ),
migrations.AddField(
model_name='food',
name='properties_food_amount',
field=models.IntegerField(blank=True, default=100),
),
migrations.AddField(
model_name='food',
name='properties_food_unit',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.unit'),
),
migrations.AddField( migrations.AddField(
model_name='supermarket', model_name='supermarket',
name='open_data_slug', name='open_data_slug',
@ -126,45 +127,30 @@ class Migration(migrations.Migration):
name='space', name='space',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
), ),
migrations.AddField(
model_name='recipeproperty',
name='property_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cookbook.propertytype'),
),
migrations.AddField(
model_name='recipeproperty',
name='space',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
migrations.AddField( migrations.AddField(
model_name='propertytype', model_name='propertytype',
name='space', name='space',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
), ),
migrations.AddField( migrations.AddField(
model_name='foodproperty', model_name='property',
name='food',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.food'),
),
migrations.AddField(
model_name='foodproperty',
name='food_unit',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.unit'),
),
migrations.AddField(
model_name='foodproperty',
name='property_type', name='property_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cookbook.propertytype'), field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cookbook.propertytype'),
), ),
migrations.AddField( migrations.AddField(
model_name='foodproperty', model_name='property',
name='space', name='space',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
), ),
migrations.AddField(
model_name='food',
name='properties',
field=models.ManyToManyField(blank=True, to='cookbook.property'),
),
migrations.AddField( migrations.AddField(
model_name='recipe', model_name='recipe',
name='properties', name='properties',
field=models.ManyToManyField(blank=True, to='cookbook.recipeproperty'), field=models.ManyToManyField(blank=True, to='cookbook.property'),
), ),
migrations.AddConstraint( migrations.AddConstraint(
model_name='unitconversion', model_name='unitconversion',
@ -174,8 +160,4 @@ class Migration(migrations.Migration):
model_name='propertytype', model_name='propertytype',
constraint=models.UniqueConstraint(fields=('space', 'name'), name='property_type_unique_name_per_space'), constraint=models.UniqueConstraint(fields=('space', 'name'), name='property_type_unique_name_per_space'),
), ),
migrations.AddConstraint(
model_name='foodproperty',
constraint=models.UniqueConstraint(fields=('food', 'property_type', 'space'), name='food_property_unique_per_space'),
),
] ]

View File

@ -1,15 +1,14 @@
# Generated by Django 4.1.7 on 2023-05-06 20:39 # Generated by Django 4.1.9 on 2023-05-25 13:06
from django.db import migrations from django.db import migrations
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from gettext import gettext as _ from gettext import gettext as _
def migrate_old_nutrition_data(apps, schema_editor): def migrate_old_nutrition_data(apps, schema_editor):
print('Transforming nutrition information, this might take a while on large databases') print('Transforming nutrition information, this might take a while on large databases')
with scopes_disabled(): with scopes_disabled():
PropertyType = apps.get_model('cookbook', 'PropertyType') PropertyType = apps.get_model('cookbook', 'PropertyType')
RecipeProperty = apps.get_model('cookbook', 'RecipeProperty') RecipeProperty = apps.get_model('cookbook', 'Property')
Recipe = apps.get_model('cookbook', 'Recipe') Recipe = apps.get_model('cookbook', 'Recipe')
Space = apps.get_model('cookbook', 'Space') Space = apps.get_model('cookbook', 'Space')
@ -28,11 +27,10 @@ def migrate_old_nutrition_data(apps, schema_editor):
r.properties.add(rp_fat, rp_carbohydrates, rp_proteins, rp_calories) r.properties.add(rp_fat, rp_carbohydrates, rp_proteins, rp_calories)
r.nutrition = None r.nutrition = None
r.save() r.save()
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('cookbook', '0189_foodproperty_propertytype_recipeproperty_and_more'), ('cookbook', '0189_property_propertytype_unitconversion_food_fdc_id_and_more'),
] ]
operations = [ operations = [

View File

@ -579,6 +579,10 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
substitute_children = models.BooleanField(default=False) substitute_children = models.BooleanField(default=False)
child_inherit_fields = models.ManyToManyField(FoodInheritField, blank=True, related_name='child_inherit') child_inherit_fields = models.ManyToManyField(FoodInheritField, blank=True, related_name='child_inherit')
properties = models.ManyToManyField("Property", blank=True)
properties_food_amount = models.IntegerField(default=100, blank=True)
properties_food_unit = models.ForeignKey(Unit, on_delete=models.PROTECT, blank=True, null=True)
preferred_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_unit') preferred_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_unit')
preferred_shopping_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_shopping_unit') preferred_shopping_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_shopping_unit')
fdc_id = models.CharField(max_length=128, null=True, blank=True, default=None) fdc_id = models.CharField(max_length=128, null=True, blank=True, default=None)
@ -787,26 +791,7 @@ class PropertyType(models.Model, PermissionModelMixin):
] ]
class FoodProperty(models.Model, PermissionModelMixin): class Property(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)
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.food_amount} {self.food_unit} {self.food}: {self.property_amount} {self.property_type.unit} {self.property_type.name}'
class Meta:
constraints = [
models.UniqueConstraint(fields=['food', 'property_type', 'space'], name='food_property_unique_per_space')
]
class RecipeProperty(models.Model, PermissionModelMixin):
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(PropertyType, on_delete=models.PROTECT) property_type = models.ForeignKey(PropertyType, on_delete=models.PROTECT)
@ -855,7 +840,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) properties = models.ManyToManyField(Property, 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

@ -32,8 +32,8 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu
RecipeBookEntry, RecipeImport, ShareLink, ShoppingList, RecipeBookEntry, RecipeImport, 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, FoodProperty, SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, Property,
PropertyType, RecipeProperty) PropertyType, Property)
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
@ -502,6 +502,36 @@ class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer):
fields = ('id', 'name', 'description', 'category_to_supermarket', 'open_data_slug') fields = ('id', 'name', 'description', 'category_to_supermarket', 'open_data_slug')
class PropertyTypeSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
if property_type := PropertyType.objects.filter(Q(name=validated_data['name'])).first():
return property_type
return super().create(validated_data)
class Meta:
model = PropertyType
fields = ('id', 'name', 'icon', 'unit', 'description', 'open_data_slug')
class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
property_type = PropertyTypeSerializer()
property_amount = CustomDecimalField()
# TODO prevent updates
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class Meta:
model = Property
fields = ('id', 'property_amount', 'property_type')
read_only_fields = ('id',)
class RecipeSimpleSerializer(WritableNestedModelSerializer): class RecipeSimpleSerializer(WritableNestedModelSerializer):
url = serializers.SerializerMethodField('get_url') url = serializers.SerializerMethodField('get_url')
@ -538,6 +568,9 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
substitute_onhand = serializers.SerializerMethodField('get_substitute_onhand') substitute_onhand = serializers.SerializerMethodField('get_substitute_onhand')
substitute = FoodSimpleSerializer(many=True, allow_null=True, required=False) substitute = FoodSimpleSerializer(many=True, allow_null=True, required=False)
properties = PropertySerializer(many=True, allow_null=True, required=False)
properties_food_unit = UnitSerializer(allow_null=True, required=False)
recipe_filter = 'steps__ingredients__food' recipe_filter = 'steps__ingredients__food'
images = ['recipe__image'] images = ['recipe__image']
@ -569,7 +602,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
# return ShoppingListEntry.objects.filter(space=obj.space, food=obj, checked=False).count() > 0 # return ShoppingListEntry.objects.filter(space=obj.space, food=obj, checked=False).count() > 0
def create(self, validated_data): def create(self, validated_data):
name = validated_data.pop('name').strip() name = validated_data['name'].strip()
if plural_name := validated_data.pop('plural_name', None): if plural_name := validated_data.pop('plural_name', None):
plural_name = plural_name.strip() plural_name = plural_name.strip()
@ -629,7 +662,9 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
class Meta: class Meta:
model = Food model = Food
fields = ( fields = (
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'food_onhand', 'supermarket_category', 'id', 'name', 'plural_name', 'description', 'shopping', 'recipe',
'properties', 'properties_food_amount', 'properties_food_unit',
'food_onhand', 'supermarket_category',
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping', 'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields', 'open_data_slug', 'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields', 'open_data_slug',
) )
@ -751,53 +786,6 @@ class UnitConversionSerializer(WritableNestedModelSerializer):
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 PropertyTypeSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
if property_type := PropertyType.objects.filter(Q(name=validated_data['name']) ).first():
return property_type
return super().create(validated_data)
class Meta:
model = PropertyType
fields = ('id', 'name', 'icon', 'unit', 'description', 'open_data_slug')
class FoodPropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
property_type = PropertyTypeSerializer()
food = FoodSimpleSerializer()
food_unit = UnitSerializer()
food_amount = CustomDecimalField()
property_amount = CustomDecimalField()
# TODO prevent updates
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class Meta:
model = FoodProperty
fields = ('id', 'food_amount', 'food_unit', 'food', 'property_amount', 'property_type')
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()
@ -848,7 +836,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) properties = PropertySerializer(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)

View File

@ -5,7 +5,7 @@ from decimal import Decimal
from cookbook.helper.cache_helper import CacheHelper from cookbook.helper.cache_helper import CacheHelper
from cookbook.helper.property_helper import FoodPropertyHelper from cookbook.helper.property_helper import FoodPropertyHelper
from cookbook.models import Unit, Food, PropertyType, FoodProperty, Recipe, Step, UnitConversion from cookbook.models import Unit, Food, PropertyType, Property, Recipe, Step, UnitConversion, Property
def test_food_property(space_1, space_2, u1_s1): def test_food_property(space_1, space_2, u1_s1):
@ -17,21 +17,23 @@ def test_food_property(space_1, space_2, u1_s1):
unit_floz2 = Unit.objects.create(name='fl. oz 2', base_unit='fluid_ounce', space=space_1) unit_floz2 = Unit.objects.create(name='fl. oz 2', base_unit='fluid_ounce', space=space_1)
unit_fantasy = Unit.objects.create(name='Fantasy Unit', base_unit='', space=space_1) unit_fantasy = Unit.objects.create(name='Fantasy Unit', base_unit='', space=space_1)
food_1 = Food.objects.create(name='food_1', space=space_1) food_1 = Food.objects.create(name='food_1', space=space_1, properties_food_unit=unit_gram, properties_food_amount=100)
food_2 = Food.objects.create(name='food_2', space=space_1) food_2 = Food.objects.create(name='food_2', space=space_1, properties_food_unit=unit_gram, properties_food_amount=100)
property_fat = PropertyType.objects.create(name='property_fat', 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_calories = PropertyType.objects.create(name='property_calories', space=space_1)
property_nuts = PropertyType.objects.create(name='property_nuts', 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) 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 = Property.objects.create(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 = Property.objects.create(property_amount=1, property_type=property_nuts, space=space_1)
food_1_property_price = FoodProperty.objects.create(food_amount=100, food_unit=unit_gram, food=food_1, property_amount=7.50, property_type=property_price, space=space_1) food_1_property_price = Property.objects.create(property_amount=7.50, property_type=property_price, space=space_1)
food_1.properties.add(food_1_property_fat, food_1_property_nuts, food_1_property_price)
food_2_property_fat = FoodProperty.objects.create(food_amount=100, food_unit=unit_gram, food=food_2, property_amount=25, property_type=property_fat, space=space_1) food_2_property_fat = Property.objects.create(property_amount=25, property_type=property_fat, space=space_1)
food_2_property_nuts = FoodProperty.objects.create(food_amount=100, food_unit=unit_gram, food=food_2, property_amount=0, property_type=property_nuts, space=space_1) food_2_property_nuts = Property.objects.create(property_amount=0, property_type=property_nuts, space=space_1)
food_2_property_price = FoodProperty.objects.create(food_amount=100, food_unit=unit_gram, food=food_2, property_amount=2.50, property_type=property_price, space=space_1) food_2_property_price = Property.objects.create(property_amount=2.50, property_type=property_price, space=space_1)
food_2.properties.add(food_2_property_fat, food_2_property_nuts, food_2_property_price)
print('\n----------- TEST PROPERTY - PROPERTY CALCULATION MULTI STEP IDENTICAL UNIT ---------------') print('\n----------- TEST PROPERTY - PROPERTY CALCULATION MULTI STEP IDENTICAL UNIT ---------------')
recipe_1 = Recipe.objects.create(name='recipe_1', waiting_time=0, working_time=0, space=space_1, created_by=auth.get_user(u1_s1)) recipe_1 = Recipe.objects.create(name='recipe_1', waiting_time=0, working_time=0, space=space_1, created_by=auth.get_user(u1_s1))

View File

@ -42,7 +42,7 @@ router.register(r'recipe-book', api.RecipeBookViewSet)
router.register(r'recipe-book-entry', api.RecipeBookEntryViewSet) router.register(r'recipe-book-entry', api.RecipeBookEntryViewSet)
router.register(r'unit-conversion', api.UnitConversionViewSet) router.register(r'unit-conversion', api.UnitConversionViewSet)
router.register(r'food-property-type', api.PropertyTypeViewSet) router.register(r'food-property-type', api.PropertyTypeViewSet)
router.register(r'food-property', api.FoodPropertyViewSet) router.register(r'food-property', api.PropertyViewSet)
router.register(r'shopping-list', api.ShoppingListViewSet) router.register(r'shopping-list', api.ShoppingListViewSet)
router.register(r'shopping-list-entry', api.ShoppingListEntryViewSet) router.register(r'shopping-list-entry', api.ShoppingListEntryViewSet)
router.register(r'shopping-list-recipe', api.ShoppingListRecipeViewSet) router.register(r'shopping-list-recipe', api.ShoppingListRecipeViewSet)

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, PropertyType, FoodProperty) SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, PropertyType, Property)
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, PropertyTypeSerializer, FoodPropertySerializer) RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer, PropertySerializer)
from cookbook.views.import_export import get_integration from cookbook.views.import_export import get_integration
from recipes import settings from recipes import settings
@ -251,7 +251,7 @@ class MergeMixin(ViewSetMixin):
try: try:
if isinstance(source, Food): if isinstance(source, Food):
FoodProperty.objects.filter(food=source).delete() source.properties.through.objects.all().delete()
for link in [field for field in source._meta.get_fields() if issubclass(type(field), ForeignObjectRel)]: for link in [field for field in source._meta.get_fields() if issubclass(type(field), ForeignObjectRel)]:
linkManager = getattr(source, link.get_accessor_name()) linkManager = getattr(source, link.get_accessor_name())
@ -825,8 +825,8 @@ class RecipeViewSet(viewsets.ModelViewSet):
'steps__ingredients__step_set', 'steps__ingredients__step_set',
'steps__ingredients__step_set__recipe_set', 'steps__ingredients__step_set__recipe_set',
'steps__ingredients__food', 'steps__ingredients__food',
'steps__ingredients__food__foodproperty_set', 'steps__ingredients__food__properties',
'steps__ingredients__food__foodproperty_set__property_type', 'steps__ingredients__food__properties__property_type',
'steps__ingredients__food__inherit_fields', 'steps__ingredients__food__inherit_fields',
'steps__ingredients__food__supermarket_category', 'steps__ingredients__food__supermarket_category',
'steps__ingredients__food__onhand_users', 'steps__ingredients__food__onhand_users',
@ -987,22 +987,12 @@ class PropertyTypeViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space) return self.queryset.filter(space=self.request.space)
class FoodPropertyViewSet(viewsets.ModelViewSet): class PropertyViewSet(viewsets.ModelViewSet):
queryset = FoodProperty.objects queryset = Property.objects
serializer_class = FoodPropertySerializer serializer_class = PropertySerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
query_params = [
QueryParam(name='food',
description=_('ID of food to return properties for.'),
qtype='int'),
]
schema = QueryParamAutoSchema()
def get_queryset(self): def get_queryset(self):
if food := self.request.query_params.get('food', None):
self.queryset = self.queryset.filter(food__id=food)
return self.queryset.filter(space=self.request.space) return self.queryset.filter(space=self.request.space)

View File

@ -1,14 +1,11 @@
import os import os
import re import re
import uuid
from datetime import datetime from datetime import datetime
from uuid import UUID from uuid import UUID
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.password_validation import validate_password from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -18,12 +15,9 @@ from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from oauth2_provider.models import AccessToken
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, ShoppingPreferenceForm, from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, SpaceCreateForm, SpaceJoinForm, User,
SpaceCreateForm, SpaceJoinForm, User, UserCreateForm, UserPreference)
UserCreateForm, UserNameForm, UserPreference, UserPreferenceForm)
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

@ -28,25 +28,77 @@
<!-- Food properties --> <!-- Food properties -->
<h5><i class="fas fa-database"></i> {{ $t('Properties') }}</h5> <h5><i class="fas fa-database"></i> {{ $t('Properties') }}</h5>
<table class="table table-bordered" v-if="food_properties">
<tr v-for="fp in food_properties" v-bind:key="fp.id"> <b-form-group :label="$t('Properties Food Amount')" description=""> <!-- TODO localize -->
<td><input v-model="fp.property_amount" type="number"> {{ fp.property_type.unit }}</td> <b-form-input v-model="food.properties_food_amount"></b-form-input>
<td><b> {{ fp.property_type.name }} </b></td> </b-form-group>
<td> /</td>
<td><input v-model="fp.food_amount" type="number"></td> <b-form-group :label="$t('Properties Food Unit')" description=""> <!-- TODO localize -->
<td>
<generic-multiselect <generic-multiselect
@change="fp.food_unit = $event.val;" @change="food.properties_food_unit = $event.val;"
:model="Models.UNIT" :model="Models.UNIT"
:initial_single_selection="fp.food_unit" :initial_single_selection="food.properties_food_unit"
label="name" label="name"
:multiple="false" :multiple="false"
:placeholder="$t('Unit')" :placeholder="$t('Unit')"
></generic-multiselect> ></generic-multiselect>
</b-form-group>
<table class="table table-bordered">
<thead>
<tr>
<th> {{ $t('Property Amount') }}</th> <!-- TODO localize -->
<th> {{ $t('Property Type') }}</th> <!-- TODO localize -->
<th></th>
<th></th>
</tr>
</thead>
<tr v-for="fp in food.properties" v-bind:key="fp.id">
<td><input v-model="fp.property_amount" type="number"> <span
v-if="fp.property_type">{{ fp.property_type.unit }}</span></td>
<td>
<generic-multiselect
@change="fp.property_type = $event.val"
:initial_single_selection="fp.property_type"
label="name" :model="Models.PROPERTY_TYPE"
:multiple="false"/>
</td>
<td> / <span>{{ food.properties_food_amount }} <span
v-if="food.properties_food_unit !== null">{{
food.properties_food_unit.name
}}</span></span>
</td>
<td>
<button class="btn btn-danger btn-small" @click="deleteProperty(fp)"><i
class="fas fa-trash-alt"></i></button>
</td> </td>
</tr> </tr>
</table> </table>
<div class="text-center">
<b-button-group>
<b-btn class="btn btn-success shadow-none" @click="addProperty()"><i
class="fa fa-plus"></i>
</b-btn>
<b-btn class="btn btn-secondary shadow-none" @click="addAllProperties()"><i
class="fa fa-plus"> <i class="ml-1 fas fa-list"></i></i>
</b-btn>
</b-button-group>
</div>
<b-form-group :label="$t('Shopping_Category')" :description="$t('shopping_category_help')">
<generic-multiselect
@change="food.supermarket_category = $event.val;"
:model="Models.SHOPPING_CATEGORY"
:initial_single_selection="food.supermarket_category"
label="name"
:multiple="false"
:allow_create="true"
:placeholder="$t('Shopping_Category')"
></generic-multiselect>
</b-form-group>
<!-- Unit conversion --> <!-- Unit conversion -->
@ -74,18 +126,6 @@
</b-form-checkbox> </b-form-checkbox>
</b-form-group> </b-form-group>
<b-form-group :label="$t('Shopping_Category')" :description="$t('shopping_category_help')">
<generic-multiselect
@change="food.supermarket_category = $event.val;"
:model="Models.SHOPPING_CATEGORY"
:initial_single_selection="food.supermarket_category"
label="name"
:multiple="false"
:allow_create="true"
:placeholder="$t('Shopping_Category')"
></generic-multiselect>
</b-form-group>
<hr/> <hr/>
<!-- todo add conditions if false disable dont hide --> <!-- todo add conditions if false disable dont hide -->
<b-form-group :label="$t('Substitutes')" :description="$t('substitute_help')"> <b-form-group :label="$t('Substitutes')" :description="$t('substitute_help')">
@ -191,7 +231,6 @@ export default {
data() { data() {
return { return {
food: undefined, food: undefined,
food_properties: [],
} }
}, },
mounted() { mounted() {
@ -212,6 +251,9 @@ export default {
description: "", description: "",
shopping: false, shopping: false,
recipe: null, recipe: null,
properties: [],
properties_food_amount: 100,
properties_food_unit: null,
food_onhand: false, food_onhand: false,
supermarket_category: null, supermarket_category: null,
parent: null, parent: null,
@ -225,85 +267,41 @@ export default {
child_inherit_fields: [], child_inherit_fields: [],
} }
} }
Promise.allSettled([pf]).then(r => {
let property_types = []
let property_values = []
let p1 = apiClient.listPropertyTypes().then((r) => {
property_types = r.data
})
let p2
if (this.food.id !== undefined) {
p2 = apiClient.listFoodPropertys(this.food.id).then((r) => {
property_values = r.data
})
}
Promise.allSettled([p1, p2]).then(r => {
property_types.forEach(fpt => {
let food_property = {
'food_amount': 100,
'food_unit': {'name': 'g'},
'food': this.food,
'property_amount': 0,
'property_type': fpt,
}
property_values.forEach(fpv => {
if (fpv.property_type.id === fpt.id) {
food_property.id = fpv.id
food_property.food_amount = fpv.food_amount
food_property.food_unit = fpv.food_unit
food_property.property_amount = fpv.property_amount
}
})
this.food_properties.push(food_property)
})
})
})
}, },
methods: { methods: {
updateFood: function () { updateFood: function () {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
let p
if (this.food.id !== undefined) { if (this.food.id !== undefined) {
p = apiClient.updateFood(this.food.id, this.food).then((r) => { apiClient.updateFood(this.food.id, this.food).then((r) => {
this.food = r.data this.food = r.data
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE) StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch(err => { }).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err) StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
}) })
} else { } else {
p = apiClient.createFood(this.food).then((r) => { apiClient.createFood(this.food).then((r) => {
this.food = r.data this.food = r.data
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE) StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
}).catch(err => { }).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err) StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
}) })
} }
},
Promise.allSettled([p]).then(r => { addProperty: function () {
this.food_properties.forEach(fp => { this.food.properties.push({property_type: null, property_amount: 0})
fp.food = this.food },
if (fp.id === undefined) { addAllProperties: function () {
apiClient.createFoodProperty(fp).then((r) => { let apiClient = new ApiApiFactory()
fp = r.data apiClient.listPropertyTypes().then(r => {
r.data.forEach(x => {
this.food.properties.push({property_type: x, property_amount: 0})
})
}).catch(err => { }).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err) StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
}) })
} else { },
apiClient.updateFoodProperty(fp.id, fp).then((r) => { deleteProperty: function (p) {
fp = r.data this.food.properties = this.food.properties.filter(x => x !== p)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
}
})
})
}, },
cancelAction: function () { cancelAction: function () {
this.$emit("hidden", "") this.$emit("hidden", "")

File diff suppressed because it is too large Load Diff