first food property UI prototype

This commit is contained in:
vabene1111 2023-04-04 13:13:51 +02:00
parent 25c914606e
commit 2a6fc723d0
5 changed files with 675 additions and 548 deletions

View File

@ -25,7 +25,7 @@ class FoodPropertyHelper:
ingredients += s.ingredients.all() ingredients += s.ingredients.all()
for fpt in property_types: # TODO is this safe or should I require the request context? for fpt in property_types: # TODO is this safe or should I require the request context?
computed_properties[fpt.id] = {'name': fpt.name, 'food_values': {}, 'total_value': 0} computed_properties[fpt.id] = {'id': fpt.id, 'name': fpt.name, 'icon': fpt.icon, 'description': fpt.description, 'unit': fpt.unit, 'food_values': {}, 'total_value': 0}
# TODO unit conversion support # TODO unit conversion support
@ -34,18 +34,18 @@ class FoodPropertyHelper:
p = i.food.foodproperty_set.filter(space=self.space, property_type=pt).first() p = i.food.foodproperty_set.filter(space=self.space, property_type=pt).first()
if p: if p:
computed_properties[p.property_type.id]['total_value'] += (i.amount / p.food_amount) * p.property_amount computed_properties[p.property_type.id]['total_value'] += (i.amount / p.food_amount) * p.property_amount
computed_properties[p.property_type.id]['food_values'] = self.add_or_create(computed_properties[p.property_type.id]['food_values'], i.food.id, (i.amount / p.food_amount) * p.property_amount) computed_properties[p.property_type.id]['food_values'] = self.add_or_create(computed_properties[p.property_type.id]['food_values'], i.food.id, (i.amount / p.food_amount) * p.property_amount, i.food)
else: else:
computed_properties[pt.id]['food_values'][i.food.id] = None computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': i.food.name, 'value': 0}
return computed_properties return computed_properties
# small dict helper to add to existing key or create new, probably a better way of doing this # small dict helper to add to existing key or create new, probably a better way of doing this
# TODO move to central helper ? # TODO move to central helper ?
@staticmethod @staticmethod
def add_or_create(d, key, value): def add_or_create(d, key, value, food):
if key in d: if key in d:
d[key] += value d[key]['value'] += value
else: else:
d[key] = value d[key] = {'id': food.id, 'food': food.name, 'value': value}
return d return d

View File

@ -22,9 +22,10 @@ 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.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 get_conversions from cookbook.helper.unit_conversion_helper import UnitConversionHelper
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, CustomFilter, from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, CustomFilter,
ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink, ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink,
Keyword, MealPlan, MealType, NutritionInformation, Recipe, RecipeBook, Keyword, MealPlan, MealType, NutritionInformation, Recipe, RecipeBook,
@ -637,7 +638,6 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer):
used_in_recipes = serializers.SerializerMethodField('get_used_in_recipes') used_in_recipes = serializers.SerializerMethodField('get_used_in_recipes')
amount = CustomDecimalField() amount = CustomDecimalField()
conversions = serializers.SerializerMethodField('get_conversions') conversions = serializers.SerializerMethodField('get_conversions')
nutritions = serializers.SerializerMethodField('get_nutritions')
def get_used_in_recipes(self, obj): def get_used_in_recipes(self, obj):
used_in = [] used_in = []
@ -647,27 +647,14 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer):
return used_in return used_in
def get_conversions(self, obj): def get_conversions(self, obj):
conversions = []
# TODO add hardcoded base conversions for metric/imperial
if obj.unit and obj.food: if obj.unit and obj.food:
conversions += get_conversions(obj.amount, obj.unit, obj.food) uch = UnitConversionHelper(self.context['request'].space)
conversions = []
return conversions for c in uch.get_conversions(obj):
conversions.append({'food': c.food.name, 'unit': c.unit.name , 'amount': c.amount}) # TODO do formatting in helper
def get_nutritions(self, ingredient): return conversions
nutritions = {} else:
return []
if ingredient.food:
for fn in ingredient.food.foodnutrition_set.all():
if fn.food_unit == ingredient.unit:
nutritions[fn.property_type.id] = ingredient.amount / fn.food_amount * fn.property_amount
else:
conversions = self.get_conversions(ingredient)
for c in conversions:
if fn.food_unit.id == c['unit']['id']:
nutritions[fn.property_type.id] = c['amount'] / fn.food_amount * fn.property_amount
return nutritions
def create(self, validated_data): def create(self, validated_data):
validated_data['space'] = self.context['request'].space validated_data['space'] = self.context['request'].space
@ -680,10 +667,11 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer):
class Meta: class Meta:
model = Ingredient model = Ingredient
fields = ( fields = (
'id', 'food', 'unit', 'amount', 'nutritions', 'conversions', 'note', 'order', 'id', 'food', 'unit', 'amount', 'conversions', 'note', 'order',
'is_header', 'no_amount', 'original_text', 'used_in_recipes', 'is_header', 'no_amount', 'original_text', 'used_in_recipes',
'always_use_plural_unit', 'always_use_plural_food', 'always_use_plural_unit', 'always_use_plural_food',
) )
read_only_fields = ['conversions', ]
class IngredientSerializer(IngredientSimpleSerializer): class IngredientSerializer(IngredientSimpleSerializer):
@ -817,17 +805,22 @@ class RecipeSerializer(RecipeBaseSerializer):
shared = UserSerializer(many=True, required=False) shared = UserSerializer(many=True, required=False)
rating = CustomDecimalField(required=False, allow_null=True, read_only=True) rating = CustomDecimalField(required=False, allow_null=True, read_only=True)
last_cooked = serializers.DateTimeField(required=False, allow_null=True, read_only=True) last_cooked = serializers.DateTimeField(required=False, allow_null=True, read_only=True)
food_properties = serializers.SerializerMethodField('get_food_properties')
def get_food_properties(self, obj):
fph = FoodPropertyHelper(self.context['request'].space)
return fph.calculate_recipe_properties(obj)
class Meta: class Meta:
model = Recipe model = Recipe
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', 'servings', 'file_path', 'servings_text', 'rating', 'internal', 'show_ingredient_overview', 'nutrition', 'food_properties', 'servings', 'file_path', 'servings_text', 'rating',
'last_cooked', 'last_cooked',
'private', 'shared', 'private', 'shared',
) )
read_only_fields = ['image', 'created_by', 'created_at'] read_only_fields = ['image', 'created_by', 'created_at', 'food_properties']
def validate(self, data): def validate(self, data):
above_limit, msg = above_space_limit(self.context['request'].space) above_limit, msg = above_space_limit(self.context['request'].space)
@ -959,11 +952,11 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
value = value.quantize( value = value.quantize(
Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
return ( return (
obj.name obj.name
or getattr(obj.mealplan, 'title', None) or getattr(obj.mealplan, 'title', None)
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)]) or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
or obj.recipe.name or obj.recipe.name
) + f' ({value:.2g})' ) + f' ({value:.2g})'
def update(self, instance, validated_data): def update(self, instance, validated_data):
# TODO remove once old shopping list # TODO remove once old shopping list

View File

@ -1,102 +1,37 @@
<template> <template>
<div id="app"> <div id="app">
<div class="row" v-if="food">
<div class="col-12">
<h2>{{ food.name }}</h2> <div class="row" v-if="recipe !== undefined">
<div class="col-6">
<div class="card">
<table>
<tbody v-for="p in recipe.food_properties" v-bind:key="`id_${p.id}`">
<tr>
<td><b-button v-b-toggle="`id_collapse_property_${p.id}`" size="sm"><i class="fas fa-caret-right"></i></b-button></td>
<td>{{ p.icon }}</td>
<td>{{ p.name }}</td>
<td>{{ p.total_value }} {{ p.unit }}</td>
</tr>
<b-collapse :id="`id_collapse_property_${p.id}`" class="mt-2">
<tr>
<td colspan="4">
{{p.description}}
</td>
</tr>
<tr v-for="f in p.food_values" v-bind:key="`id_${p.id}_food_${f.id}`">
<td>{{f.food}}</td>
<td>{{f.value}} {{ p.unit }}</td>
</tr>
</b-collapse>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
<div class="row">
<div class="col-12">
<b-form v-if="food">
<b-form-group :label="$t('Name')" description="">
<b-form-input v-model="food.name"></b-form-input>
</b-form-group>
<b-form-group :label="$t('Plural')" description="">
<b-form-input v-model="food.plural_name"></b-form-input>
</b-form-group>
<b-form-group :label="$t('Recipe')" :description="$t('food_recipe_help')">
<generic-multiselect
@change="food.recipe = $event.val;"
:model="Models.RECIPE"
:initial_selection="food.recipe"
label="name"
:multiple="false"
:placeholder="$t('Recipe')"
></generic-multiselect>
</b-form-group>
<b-form-group :description="$t('OnHand_help')">
<b-form-checkbox v-model="food.food_onhand">{{ $t('OnHand') }}</b-form-checkbox>
</b-form-group>
<b-form-group :description="$t('ignore_shopping_help')">
<b-form-checkbox v-model="food.ignore_shopping">{{ $t('Ignore_Shopping') }}</b-form-checkbox>
</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_selection="food.supermarket_category"
label="name"
:multiple="false"
:placeholder="$t('Shopping_Category')"
></generic-multiselect>
</b-form-group>
<hr/>
<!-- todo add conditions if false disable dont hide -->
<b-form-group :label="$t('Substitutes')" :description="$t('substitute_help')">
<generic-multiselect
@change="food.substitute = $event.val;"
:model="Models.FOOD"
:initial_selection="food.substitute"
label="name"
:multiple="false"
:placeholder="$t('Substitutes')"
></generic-multiselect>
</b-form-group>
<b-form-group :description="$t('substitute_siblings_help')">
<b-form-checkbox v-model="food.substitute_siblings">{{ $t('substitute_siblings') }}</b-form-checkbox>
</b-form-group>
<b-form-group :label="$t('InheritFields')" :description="$t('InheritFields_help')">
<generic-multiselect
@change="food.inherit_fields = $event.val;"
:model="Models.FOOD_INHERIT_FIELDS"
:initial_selection="food.inherit_fields"
label="name"
:multiple="false"
:placeholder="$t('InheritFields')"
></generic-multiselect>
</b-form-group>
<b-form-group :label="$t('ChildInheritFields')" :description="$t('ChildInheritFields_help')">
<generic-multiselect
@change="food.child_inherit_fields = $event.val;"
:model="Models.FOOD_INHERIT_FIELDS"
:initial_selection="food.child_inherit_fields"
label="name"
:multiple="false"
:placeholder="$t('ChildInheritFields')"
></generic-multiselect>
</b-form-group>
<!-- TODO change to a button -->
<b-form-group :description="$t('reset_children_help')">
<b-form-checkbox v-model="food.reset_inherit">{{ $t('reset_children') }}</b-form-checkbox>
</b-form-group>
<b-button variant="primary" @click="updateFood">{{ $t('Save') }}</b-button>
</b-form>
</div>
</div>
</div> </div>
</template> </template>
@ -108,9 +43,8 @@ import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css" import "bootstrap-vue/dist/bootstrap-vue.css"
import {ApiApiFactory} from "@/utils/openapi/api"; import {ApiApiFactory} from "@/utils/openapi/api";
import RecipeCard from "@/components/RecipeCard.vue";
import GenericMultiselect from "@/components/GenericMultiselect.vue"; import {ApiMixin} from "@/utils/utils";
import {ApiMixin, StandardToasts} from "@/utils/utils";
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
@ -119,34 +53,22 @@ Vue.use(BootstrapVue)
export default { export default {
name: "TestView", name: "TestView",
mixins: [ApiMixin], mixins: [ApiMixin],
components: { components: {},
GenericMultiselect
},
data() { data() {
return { return {
food: undefined, recipe: undefined,
} }
}, },
mounted() { mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE this.$i18n.locale = window.CUSTOM_LOCALE
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
apiClient.retrieveFood('1').then((r) => { apiClient.retrieveRecipe('1').then((r) => {
this.food = r.data this.recipe = r.data
}) })
}, },
methods: { methods: {},
updateFood: function () {
let apiClient = new ApiApiFactory()
apiClient.updateFood(this.food.id, this.food).then((r) => {
this.food = r.data
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
}
},
} }
</script> </script>

View File

@ -0,0 +1,155 @@
<template>
<div id="app">
<div class="row" v-if="food">
<div class="col-12">
<h2>{{ food.name }}</h2>
</div>
</div>
<div class="row">
<div class="col-12">
<b-form v-if="food">
<b-form-group :label="$t('Name')" description="">
<b-form-input v-model="food.name"></b-form-input>
</b-form-group>
<b-form-group :label="$t('Plural')" description="">
<b-form-input v-model="food.plural_name"></b-form-input>
</b-form-group>
<b-form-group :label="$t('Recipe')" :description="$t('food_recipe_help')">
<generic-multiselect
@change="food.recipe = $event.val;"
:model="Models.RECIPE"
:initial_selection="food.recipe"
label="name"
:multiple="false"
:placeholder="$t('Recipe')"
></generic-multiselect>
</b-form-group>
<b-form-group :description="$t('OnHand_help')">
<b-form-checkbox v-model="food.food_onhand">{{ $t('OnHand') }}</b-form-checkbox>
</b-form-group>
<b-form-group :description="$t('ignore_shopping_help')">
<b-form-checkbox v-model="food.ignore_shopping">{{ $t('Ignore_Shopping') }}</b-form-checkbox>
</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_selection="food.supermarket_category"
label="name"
:multiple="false"
:placeholder="$t('Shopping_Category')"
></generic-multiselect>
</b-form-group>
<hr/>
<!-- todo add conditions if false disable dont hide -->
<b-form-group :label="$t('Substitutes')" :description="$t('substitute_help')">
<generic-multiselect
@change="food.substitute = $event.val;"
:model="Models.FOOD"
:initial_selection="food.substitute"
label="name"
:multiple="false"
:placeholder="$t('Substitutes')"
></generic-multiselect>
</b-form-group>
<b-form-group :description="$t('substitute_siblings_help')">
<b-form-checkbox v-model="food.substitute_siblings">{{ $t('substitute_siblings') }}</b-form-checkbox>
</b-form-group>
<b-form-group :label="$t('InheritFields')" :description="$t('InheritFields_help')">
<generic-multiselect
@change="food.inherit_fields = $event.val;"
:model="Models.FOOD_INHERIT_FIELDS"
:initial_selection="food.inherit_fields"
label="name"
:multiple="false"
:placeholder="$t('InheritFields')"
></generic-multiselect>
</b-form-group>
<b-form-group :label="$t('ChildInheritFields')" :description="$t('ChildInheritFields_help')">
<generic-multiselect
@change="food.child_inherit_fields = $event.val;"
:model="Models.FOOD_INHERIT_FIELDS"
:initial_selection="food.child_inherit_fields"
label="name"
:multiple="false"
:placeholder="$t('ChildInheritFields')"
></generic-multiselect>
</b-form-group>
<!-- TODO change to a button -->
<b-form-group :description="$t('reset_children_help')">
<b-form-checkbox v-model="food.reset_inherit">{{ $t('reset_children') }}</b-form-checkbox>
</b-form-group>
<b-button variant="primary" @click="updateFood">{{ $t('Save') }}</b-button>
</b-form>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import {ApiApiFactory} from "@/utils/openapi/api";
import RecipeCard from "@/components/RecipeCard.vue";
import GenericMultiselect from "@/components/GenericMultiselect.vue";
import {ApiMixin, StandardToasts} from "@/utils/utils";
Vue.use(BootstrapVue)
export default {
name: "TestView",
mixins: [ApiMixin],
components: {
GenericMultiselect
},
data() {
return {
food: undefined,
}
},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
let apiClient = new ApiApiFactory()
apiClient.retrieveFood('1').then((r) => {
this.food = r.data
})
},
methods: {
updateFood: function () {
let apiClient = new ApiApiFactory()
apiClient.updateFood(this.food.id, this.food).then((r) => {
this.food = r.data
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
}
},
}
</script>
<style>
</style>

File diff suppressed because it is too large Load Diff