food layout complete, edit food working

This commit is contained in:
smilerz 2021-08-18 14:51:46 -05:00
parent b56c2429c7
commit d5e9c5d8f8
21 changed files with 138783 additions and 244 deletions

View File

@ -23,7 +23,7 @@ def backwards(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('cookbook', '0148_auto_20210813_1829'), ('cookbook', '0148_food_to_tree'),
] ]
operations = [ operations = [

View File

@ -1,9 +1,11 @@
import json
import random import random
from datetime import timedelta from datetime import timedelta
from decimal import Decimal from decimal import Decimal
from gettext import gettext as _ from gettext import gettext as _
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Avg, QuerySet, Sum from django.db.models import Avg, Manager, QuerySet, Sum
from django.urls import reverse
from drf_writable_nested import (UniqueFieldsMixin, from drf_writable_nested import (UniqueFieldsMixin,
WritableNestedModelSerializer) WritableNestedModelSerializer)
from rest_framework import serializers from rest_framework import serializers
@ -56,6 +58,24 @@ class SpaceFilterSerializer(serializers.ListSerializer):
return super().to_representation(data) return super().to_representation(data)
# custom related field, sends details on read, accepts primary key on write
# class RelatedFieldAlternative(serializers.PrimaryKeyRelatedField):
# def __init__(self, **kwargs):
# self.serializer = kwargs.pop('serializer', None)
# if self.serializer is not None and not issubclass(self.serializer, serializers.Serializer):
# raise TypeError('"serializer" is not a valid serializer class')
# super().__init__(**kwargs)
# def use_pk_only_optimization(self):
# return False if self.serializer else True
# def to_representation(self, instance):
# if self.serializer:
# return self.serializer(instance, context=self.context).data
# return super().to_representation(instance)
class SpacedModelSerializer(serializers.ModelSerializer): class SpacedModelSerializer(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
@ -286,8 +306,21 @@ class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer):
fields = ('id', 'name', 'category_to_supermarket') fields = ('id', 'name', 'category_to_supermarket')
class RecipeSimpleSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField('get_url')
def get_url(self, obj):
return reverse('view_recipe', args=[obj.id])
class Meta:
model = Recipe
fields = ('id', 'name', 'url')
read_only_fields = ['id', 'name', 'url']
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer): class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False) # RelatedFieldAlternative adds details of related object on read, accepts PK on write
# this approach prevents adding *new* objects when updating Food, SupermarketCategory must be created elsewhere
image = serializers.SerializerMethodField('get_image') image = serializers.SerializerMethodField('get_image')
numrecipe = serializers.SerializerMethodField('count_recipes') numrecipe = serializers.SerializerMethodField('count_recipes')
@ -309,6 +342,19 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
def count_recipes(self, obj): def count_recipes(self, obj):
return Recipe.objects.filter(steps__ingredients__food=obj, space=obj.space).count() return Recipe.objects.filter(steps__ingredients__food=obj, space=obj.space).count()
def to_representation(self, instance):
response = super().to_representation(instance)
# turns a GET of food.recipe into a dict of data while allowing a PATCH/PUT of an integer to update a food with a recipe
recipe = RecipeSimpleSerializer(instance.recipe, allow_null=True).data
supermarket_category = SupermarketCategorySerializer(instance.supermarket_category, allow_null=True).data
response['recipe'] = recipe if recipe else None
# the SupermarketCategorySerializer returns a dict instead of None when the column is null
if supermarket_category == {'name': ''} or None:
response['supermarket_category'] = None
else:
response['supermarket_category'] = supermarket_category
return response
def create(self, validated_data): def create(self, validated_data):
validated_data['name'] = validated_data['name'].strip() validated_data['name'] = validated_data['name'].strip()
validated_data['space'] = self.context['request'].space validated_data['space'] = self.context['request'].space
@ -321,7 +367,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
class Meta: class Meta:
model = Food model = Food
fields = ('id', 'name', 'recipe', 'ignore_shopping', 'supermarket_category', 'image', 'parent', 'numchild', 'numrecipe') fields = ('id', 'name', 'description', 'recipe', 'ignore_shopping', 'supermarket_category', 'image', 'parent', 'numchild', 'numrecipe')
read_only_fields = ('id', 'numchild', 'parent', 'image') read_only_fields = ('id', 'numchild', 'parent', 'image')

View File

@ -1 +1 @@
.shake[data-v-33424c9e]{-webkit-animation:shake-data-v-33424c9e .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-33424c9e .82s cubic-bezier(.36,.07,.19,.97) both;transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden;perspective:1000px}@-webkit-keyframes shake-data-v-33424c9e{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}@keyframes shake-data-v-33424c9e{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}} .shake[data-v-2ff37af5]{-webkit-animation:shake-data-v-2ff37af5 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-2ff37af5 .82s cubic-bezier(.36,.07,.19,.97) both;transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden;perspective:1000px}@-webkit-keyframes shake-data-v-2ff37af5{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}@keyframes shake-data-v-2ff37af5{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}

View File

@ -1 +1 @@
.shake[data-v-d5a65348]{-webkit-animation:shake-data-v-d5a65348 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-d5a65348 .82s cubic-bezier(.36,.07,.19,.97) both;transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden;perspective:1000px}@-webkit-keyframes shake-data-v-d5a65348{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}@keyframes shake-data-v-d5a65348{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}} .shake[data-v-eea8728a]{-webkit-animation:shake-data-v-eea8728a .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-eea8728a .82s cubic-bezier(.36,.07,.19,.97) both;transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden;perspective:1000px}@-webkit-keyframes shake-data-v-eea8728a{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}@keyframes shake-data-v-eea8728a{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -119,10 +119,11 @@
<label for="id_food_description_edit">{{ this.$t('Description') }}</label> <label for="id_food_description_edit">{{ this.$t('Description') }}</label>
<input class="form-control" type="text" id="id_food_description_edit" v-model="this_item.description"> <input class="form-control" type="text" id="id_food_description_edit" v-model="this_item.description">
<label for="id_food_recipe_edit">{{ this.$t('Recipe') }}</label> <label for="id_food_recipe_edit">{{ this.$t('Recipe') }}</label>
<!-- TODO initial selection isn't working and I don't know why -->
<generic-multiselect <generic-multiselect
@change="this_item.recipe=$event.val" @change="this_item.recipe=$event.val"
label="name" label="name"
:initial_selection="this_item.recipe" :initial_selection="[this_item.recipe]"
search_function="listRecipes" search_function="listRecipes"
:multiple="false" :multiple="false"
:sticky_options="[{'id': null,'name': $t('None')}]" :sticky_options="[{'id': null,'name': $t('None')}]"
@ -134,7 +135,16 @@
<label class="form-check-label" for="id_food_ignore_edit">{{ this.$t('Ignore_Shopping') }}</label> <label class="form-check-label" for="id_food_ignore_edit">{{ this.$t('Ignore_Shopping') }}</label>
</div> </div>
<label for="id_food_category_edit">{{ this.$t('Shopping_Category') }}</label> <label for="id_food_category_edit">{{ this.$t('Shopping_Category') }}</label>
<input class="form-control" type="text" id="id_food_category_edit" > <generic-multiselect
@change="this_item.supermarket_category=$event.val"
label="name"
:initial_selection="[this_item.supermarket_category]"
search_function="listSupermarketCategorys"
:multiple="false"
:sticky_options="[{'id': null,'name': $t('None')}]"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="this.$t('Shopping_Category')">
</generic-multiselect>
</form> </form>
</b-modal> </b-modal>
<!-- delete modal --> <!-- delete modal -->
@ -223,6 +233,7 @@ export default {
right: +new Date(), right: +new Date(),
isDirtyRight: false, isDirtyRight: false,
left_page: 0, left_page: 0,
update_recipe: [],
left: +new Date(), left: +new Date(),
isDirtyLeft: false, isDirtyLeft: false,
this_item: { this_item: {
@ -230,8 +241,9 @@ export default {
'name': '', 'name': '',
'description': '', 'description': '',
'recipe': null, 'recipe': null,
'ignore_shopping': '', 'recipe_full': undefined,
'supermarket_category': null, 'ignore_shopping': false,
'supermarket_category': undefined,
'target': { 'target': {
'id': -1, 'id': -1,
'name': '' 'name': ''
@ -282,6 +294,7 @@ export default {
this.$bvModal.show('id_modal_food_edit') this.$bvModal.show('id_modal_food_edit')
} else if (e.action == 'edit') { } else if (e.action == 'edit') {
this.this_item = source this.this_item = source
console.log('start edit', this.this_item)
this.$bvModal.show('id_modal_food_edit') this.$bvModal.show('id_modal_food_edit')
} else if (e.action === 'move') { } else if (e.action === 'move') {
this.this_item = source this.this_item = source
@ -316,14 +329,17 @@ export default {
}, },
saveFood: function () { saveFood: function () {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
console.log(this.this_item, !this.this_item.id) console.log('this item', this.this_item.supermarket_category?.id, this.this_item.supermarket_category?.id ?? null)
// let food = { let food = {
// name: this.this_item.name, name: this.this_item.name,
// description: this.this_item.description, description: this.this_item.description,
// icon: this.this_item.icon, recipe: this.this_item.recipe?.id ?? null,
// } ignore_shopping: this.this_item.ignore_shopping,
supermarket_category: this.this_item.supermarket_category?.id ?? null,
}
console.log('food', food)
if (!this.this_item.id) { // if there is no item id assume its a new item if (!this.this_item.id) { // if there is no item id assume its a new item
apiClient.createFood(this.this_item).then(result => { apiClient.createFood(food).then(result => {
// place all new foods at the top of the list - could sort instead // place all new foods at the top of the list - could sort instead
this.foods = [result.data].concat(this.foods) this.foods = [result.data].concat(this.foods)
// this creates a deep copy to make sure that columns stay independent // this creates a deep copy to make sure that columns stay independent
@ -338,7 +354,7 @@ export default {
this.this_item = {} this.this_item = {}
}) })
} else { } else {
apiClient.partialUpdateFood(this.this_item.id, this.this_item).then(result => { apiClient.partialUpdateFood(this.this_item.id, food).then(result => {
this.refreshCard(this.this_item.id) this.refreshCard(this.this_item.id)
this.this_item={} this.this_item={}
}).catch((err) => { }).catch((err) => {
@ -428,7 +444,6 @@ export default {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
let parent = {} let parent = {}
let pageSize = 200 let pageSize = 200
console.log(apiClient.listRecipes)
apiClient.listRecipes( apiClient.listRecipes(
undefined, undefined, String(food.id), undefined, undefined, undefined, undefined, undefined, String(food.id), undefined, undefined, undefined,
@ -583,7 +598,7 @@ export default {
} }
this.foods = this.foods.filter(kw => kw.id != id) this.foods = this.foods.filter(kw => kw.id != id)
this.foods2 = this.foods2.filter(kw => kw.id != id) this.foods2 = this.foods2.filter(kw => kw.id != id)
} },
} }
} }

View File

@ -11,7 +11,7 @@
@dragleave="handleDragLeave($event)" @dragleave="handleDragLeave($event)"
@drop="handleDragDrop($event)"> @drop="handleDragDrop($event)">
<b-row no-gutters style="height:inherit;"> <b-row no-gutters style="height:inherit;">
<b-col no-gutters md="3" style="justify-content: center; height:inherit;"> <b-col no-gutters md="3" style="height:inherit;">
<b-card-img-lazy style="object-fit: cover; height: 10vh;" :src="food_image" v-bind:alt="$t('Recipe_Image')"></b-card-img-lazy> <b-card-img-lazy style="object-fit: cover; height: 10vh;" :src="food_image" v-bind:alt="$t('Recipe_Image')"></b-card-img-lazy>
</b-col> </b-col>
<b-col no-gutters md="9" style="height:inherit;"> <b-col no-gutters md="9" style="height:inherit;">
@ -34,9 +34,10 @@
</b-card-text> </b-card-text>
</b-card-body> </b-card-body>
</b-col> </b-col>
<div class="card-img-overlay h-100 d-flex flex-column justify-content-right" <div class="card-img-overlay justify-content-right h-25 m-0 p-0 text-right">
style="float:right; text-align: right; padding-top: 10px; padding-right: 5px"> <b-button v-if="food.recipe" v-b-tooltip.hover :title="food.recipe.name"
<generic-context-menu style="float:right" class=" btn fas fa-book-open p-0 border-0" variant="link" :href="food.recipe.url"/>
<generic-context-menu class="p-0"
:show_merge="true" :show_merge="true"
:show_move="true" :show_move="true"
@item-action="$emit('item-action', {'action': $event, 'source': food})"> @item-action="$emit('item-action', {'action': $event, 'source': food})">

View File

@ -1,5 +1,4 @@
<template> <template>
<div>
<b-dropdown variant="link" toggle-class="text-decoration-none" no-caret> <b-dropdown variant="link" toggle-class="text-decoration-none" no-caret>
<template #button-content> <template #button-content>
<i class="fas fa-ellipsis-v" ></i> <i class="fas fa-ellipsis-v" ></i>
@ -21,7 +20,6 @@
</b-dropdown-item> </b-dropdown-item>
</b-dropdown> </b-dropdown>
</div>
</template> </template>
<script> <script>

View File

@ -44,11 +44,20 @@ export default {
}, },
watch: { watch: {
initial_selection: function (newVal, oldVal) { // watch it initial_selection: function (newVal, oldVal) { // watch it
this.selected_objects = newVal if (this.multiple) {
this.selected_objects = newVal
} else if (this.selected_objects != newVal?.[0]) {
// when not using multiple selections need to convert array to value
this.selected_objects = newVal?.[0] ?? null
}
}, },
}, },
mounted() { mounted() {
this.search('') this.search('')
// when not using multiple selections need to convert array to value
if (!this.multiple & this.selected_objects != this.initial_selection?.[0]) {
this.selected_objects = this.initial_selection?.[0] ?? null
}
}, },
methods: { methods: {
search: function (query) { search: function (query) {

View File

@ -11,7 +11,7 @@
@dragleave="handleDragLeave($event)" @dragleave="handleDragLeave($event)"
@drop="handleDragDrop($event)"> @drop="handleDragDrop($event)">
<b-row no-gutters style="height:inherit;"> <b-row no-gutters style="height:inherit;">
<b-col no-gutters md="3" style="justify-content: center; height:inherit;"> <b-col no-gutters md="3" style="height:inherit;">
<b-card-img-lazy style="object-fit: cover; height: 10vh;" :src="keyword_image" v-bind:alt="$t('Recipe_Image')"></b-card-img-lazy> <b-card-img-lazy style="object-fit: cover; height: 10vh;" :src="keyword_image" v-bind:alt="$t('Recipe_Image')"></b-card-img-lazy>
</b-col> </b-col>
<b-col no-gutters md="9" style="height:inherit;"> <b-col no-gutters md="9" style="height:inherit;">
@ -34,9 +34,8 @@
</b-card-text> </b-card-text>
</b-card-body> </b-card-body>
</b-col> </b-col>
<div class="card-img-overlay h-100 d-flex flex-column justify-content-right" <div class="card-img-overlay justify-content-right h-25 m-0 p-0 text-right">
style="float:right; text-align: right; padding-top: 10px; padding-right: 5px"> <generic-context-menu class="p-0"
<generic-context-menu style="float:right"
:show_merge="true" :show_merge="true"
:show_move="true" :show_move="true"
@item-action="$emit('item-action', {'action': $event, 'source': keyword})"> @item-action="$emit('item-action', {'action': $event, 'source': keyword})">

View File

@ -84,8 +84,9 @@ module.exports = {
}, },
}, },
}, },
// TODO make this conditional on .env DEBUG = FALSE },
config.optimization.minimize(true) // TODO make this conditional on .env DEBUG = TRUE
config.optimization.minimize(false)
); );
//TODO somehow remov them as they are also added to the manifest config of the service worker //TODO somehow remov them as they are also added to the manifest config of the service worker