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):
dependencies = [
('cookbook', '0148_auto_20210813_1829'),
('cookbook', '0148_food_to_tree'),
]
operations = [

View File

@ -1,9 +1,11 @@
import json
import random
from datetime import timedelta
from decimal import Decimal
from gettext import gettext as _
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,
WritableNestedModelSerializer)
from rest_framework import serializers
@ -56,6 +58,24 @@ class SpaceFilterSerializer(serializers.ListSerializer):
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):
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
@ -286,8 +306,21 @@ class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer):
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):
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')
numrecipe = serializers.SerializerMethodField('count_recipes')
@ -309,6 +342,19 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
def count_recipes(self, obj):
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):
validated_data['name'] = validated_data['name'].strip()
validated_data['space'] = self.context['request'].space
@ -321,7 +367,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
class Meta:
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')

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>
<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>
<!-- TODO initial selection isn't working and I don't know why -->
<generic-multiselect
@change="this_item.recipe=$event.val"
label="name"
:initial_selection="this_item.recipe"
:initial_selection="[this_item.recipe]"
search_function="listRecipes"
:multiple="false"
: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>
</div>
<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>
</b-modal>
<!-- delete modal -->
@ -223,6 +233,7 @@ export default {
right: +new Date(),
isDirtyRight: false,
left_page: 0,
update_recipe: [],
left: +new Date(),
isDirtyLeft: false,
this_item: {
@ -230,8 +241,9 @@ export default {
'name': '',
'description': '',
'recipe': null,
'ignore_shopping': '',
'supermarket_category': null,
'recipe_full': undefined,
'ignore_shopping': false,
'supermarket_category': undefined,
'target': {
'id': -1,
'name': ''
@ -282,6 +294,7 @@ export default {
this.$bvModal.show('id_modal_food_edit')
} else if (e.action == 'edit') {
this.this_item = source
console.log('start edit', this.this_item)
this.$bvModal.show('id_modal_food_edit')
} else if (e.action === 'move') {
this.this_item = source
@ -316,14 +329,17 @@ export default {
},
saveFood: function () {
let apiClient = new ApiApiFactory()
console.log(this.this_item, !this.this_item.id)
// let food = {
// name: this.this_item.name,
// description: this.this_item.description,
// icon: this.this_item.icon,
// }
console.log('this item', this.this_item.supermarket_category?.id, this.this_item.supermarket_category?.id ?? null)
let food = {
name: this.this_item.name,
description: this.this_item.description,
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
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
this.foods = [result.data].concat(this.foods)
// this creates a deep copy to make sure that columns stay independent
@ -338,7 +354,7 @@ export default {
this.this_item = {}
})
} 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.this_item={}
}).catch((err) => {
@ -428,7 +444,6 @@ export default {
let apiClient = new ApiApiFactory()
let parent = {}
let pageSize = 200
console.log(apiClient.listRecipes)
apiClient.listRecipes(
undefined, undefined, String(food.id), undefined, undefined, undefined,
@ -583,7 +598,7 @@ export default {
}
this.foods = this.foods.filter(kw => kw.id != id)
this.foods2 = this.foods2.filter(kw => kw.id != id)
}
},
}
}

View File

@ -11,7 +11,7 @@
@dragleave="handleDragLeave($event)"
@drop="handleDragDrop($event)">
<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-col>
<b-col no-gutters md="9" style="height:inherit;">
@ -34,9 +34,10 @@
</b-card-text>
</b-card-body>
</b-col>
<div class="card-img-overlay h-100 d-flex flex-column justify-content-right"
style="float:right; text-align: right; padding-top: 10px; padding-right: 5px">
<generic-context-menu style="float:right"
<div class="card-img-overlay justify-content-right h-25 m-0 p-0 text-right">
<b-button v-if="food.recipe" v-b-tooltip.hover :title="food.recipe.name"
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_move="true"
@item-action="$emit('item-action', {'action': $event, 'source': food})">

View File

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

View File

@ -44,11 +44,20 @@ export default {
},
watch: {
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() {
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: {
search: function (query) {

View File

@ -11,7 +11,7 @@
@dragleave="handleDragLeave($event)"
@drop="handleDragDrop($event)">
<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-col>
<b-col no-gutters md="9" style="height:inherit;">
@ -34,9 +34,8 @@
</b-card-text>
</b-card-body>
</b-col>
<div class="card-img-overlay h-100 d-flex flex-column justify-content-right"
style="float:right; text-align: right; padding-top: 10px; padding-right: 5px">
<generic-context-menu style="float:right"
<div class="card-img-overlay justify-content-right h-25 m-0 p-0 text-right">
<generic-context-menu class="p-0"
:show_merge="true"
:show_move="true"
@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