This commit is contained in:
smilerz 2021-08-16 15:01:35 -05:00
parent 0559143f0e
commit b56c2429c7
14 changed files with 84 additions and 108 deletions

View File

@ -47,7 +47,7 @@ class SpaceFilterSerializer(serializers.ListSerializer):
def to_representation(self, data): def to_representation(self, data):
if (type(data) == QuerySet and data.query.is_sliced): if (type(data) == QuerySet and data.query.is_sliced):
# if query is sliced or if is a MP_NodeQuerySet it came from api request not nested serializer # if query is sliced it came from api request not nested serializer
return super().to_representation(data) return super().to_representation(data)
if self.child.Meta.model == User: if self.child.Meta.model == User:
data = data.filter(userpreference__space=self.context['request'].space) data = data.filter(userpreference__space=self.context['request'].space)
@ -212,8 +212,8 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
def get_image(self, obj): def get_image(self, obj):
recipes = obj.recipe_set.all().filter(space=obj.space).exclude(image__isnull=True).exclude(image__exact='') recipes = obj.recipe_set.all().filter(space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
if len(recipes) == 0: if len(recipes) == 0 and obj.has_children():
recipes = Recipe.objects.filter(keywords__in=obj.get_tree(), space=obj.space).exclude(image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree recipes = Recipe.objects.filter(keywords__in=obj.get_descendants(), space=obj.space).exclude(image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
if len(recipes) != 0: if len(recipes) != 0:
return random.choice(recipes).image.url return random.choice(recipes).image.url
else: else:
@ -297,10 +297,9 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
return obj.recipe.image.url return obj.recipe.image.url
# if food is not also a recipe, look for recipe images that use the food # if food is not also a recipe, look for recipe images that use the food
recipes = Recipe.objects.filter(steps__ingredients__food=obj, space=obj.space).exclude(image__isnull=True).exclude(image__exact='') recipes = Recipe.objects.filter(steps__ingredients__food=obj, space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
# if no recipes found - check whole tree # if no recipes found - check whole tree
if len(recipes) == 0: if len(recipes) == 0 and obj.has_children():
recipes = Recipe.objects.filter(steps__ingredients__food__in=obj.get_tree(), space=obj.space).exclude(image__isnull=True).exclude(image__exact='') recipes = Recipe.objects.filter(steps__ingredients__food__in=obj.get_descendants(), space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
if len(recipes) != 0: if len(recipes) != 0:
return random.choice(recipes).image.url return random.choice(recipes).image.url

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

@ -135,10 +135,6 @@ urlpatterns = [
name='web_manifest'), name='web_manifest'),
] ]
# generic_models = (
# Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync,
# Comment, RecipeBookEntry, Keyword, Food, ShoppingList, InviteLink
# )
generic_models = ( generic_models = (
Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync, Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync,
Comment, RecipeBookEntry, ShoppingList, InviteLink Comment, RecipeBookEntry, ShoppingList, InviteLink

View File

@ -80,7 +80,7 @@
spinner="waveDots"> spinner="waveDots">
</infinite-loading> </infinite-loading>
</div> </div>
<!-- right side keyword cards --> <!-- right side food cards -->
<div class="col col-md mh-100 overflow-auto " v-if="show_split"> <div class="col col-md mh-100 overflow-auto " v-if="show_split">
<food-card <food-card
v-for="f in foods2" v-for="f in foods2"
@ -108,51 +108,56 @@
<!-- TODO Modals can probably be made generic and moved to component --> <!-- TODO Modals can probably be made generic and moved to component -->
<!-- edit modal --> <!-- edit modal -->
<b-modal class="modal" <b-modal class="modal"
:id="'id_modal_keyword_edit'" :id="'id_modal_food_edit'"
@shown="prepareEmoji" :title="this.$t('Edit_Food')"
:title="this.$t('Edit_Keyword')"
:ok-title="this.$t('Save')" :ok-title="this.$t('Save')"
:cancel-title="this.$t('Cancel')" :cancel-title="this.$t('Cancel')"
@ok="saveKeyword"> @ok="saveFood">
<form> <form>
<label for="id_keyword_name_edit">{{ this.$t('Name') }}</label> <label for="id_food_name_edit">{{ this.$t('Name') }}</label>
<input class="form-control" type="text" id="id_keyword_name_edit" v-model="this_item.name"> <input class="form-control" type="text" id="id_food_name_edit" v-model="this_item.name">
<label for="id_keyword_description_edit">{{ this.$t('Description') }}</label> <label for="id_food_description_edit">{{ this.$t('Description') }}</label>
<input class="form-control" type="text" id="id_keyword_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_keyword_icon_edit">{{ this.$t('Icon') }}</label> <label for="id_food_recipe_edit">{{ this.$t('Recipe') }}</label>
<twemoji-textarea <generic-multiselect
id="id_keyword_icon_edit" @change="this_item.recipe=$event.val"
ref="_edit" label="name"
:emojiData="emojiDataAll" :initial_selection="this_item.recipe"
:emojiGroups="emojiGroups" search_function="listRecipes"
triggerType="hover" :multiple="false"
recentEmojisFeat="true" :sticky_options="[{'id': null,'name': $t('None')}]"
recentEmojisStorage="local" style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
@contentChanged="setIcon" :placeholder="this.$t('Search')">
/> </generic-multiselect>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="id_food_ignore_edit" v-model="this_item.ignore_shopping">
<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" >
</form> </form>
</b-modal> </b-modal>
<!-- delete modal --> <!-- delete modal -->
<b-modal class="modal" <b-modal class="modal"
:id="'id_modal_keyword_delete'" :id="'id_modal_food_delete'"
:title="this.$t('Delete_Keyword')" :title="this.$t('Delete_Food')"
:ok-title="this.$t('Delete')" :ok-title="this.$t('Delete')"
:cancel-title="this.$t('Cancel')" :cancel-title="this.$t('Cancel')"
@ok="delKeyword(this_item.id)"> @ok="delFood(this_item.id)">
{{this.$t("delete_confimation", {'kw': this_item.name})}} {{this_item.name}} {{this.$t("delete_confimation", {'kw': this_item.name})}} {{this_item.name}}
</b-modal> </b-modal>
<!-- move modal --> <!-- move modal -->
<b-modal class="modal" <b-modal class="modal"
:id="'id_modal_keyword_move'" :id="'id_modal_food_move'"
:title="this.$t('Move_Keyword')" :title="this.$t('Move_Food')"
:ok-title="this.$t('Move')" :ok-title="this.$t('Move')"
:cancel-title="this.$t('Cancel')" :cancel-title="this.$t('Cancel')"
@ok="moveKeyword(this_item.id, this_item.target.id)"> @ok="moveFood(this_item.id, this_item.target.id)">
{{ this.$t("move_selection", {'child': this_item.name}) }} {{ this.$t("move_selection", {'child': this_item.name}) }}
<generic-multiselect <generic-multiselect
@change="this_item.target=$event.val" @change="this_item.target=$event.val"
label="name" label="name"
search_function="listKeywords" search_function="listFood"
:multiple="false" :multiple="false"
:sticky_options="[{'id': 0,'name': $t('Root')}]" :sticky_options="[{'id': 0,'name': $t('Root')}]"
:tree_api="true" :tree_api="true"
@ -162,16 +167,16 @@
</b-modal> </b-modal>
<!-- merge modal --> <!-- merge modal -->
<b-modal class="modal" <b-modal class="modal"
:id="'id_modal_keyword_merge'" :id="'id_modal_food_merge'"
:title="this.$t('Merge_Keyword')" :title="this.$t('Merge_Food')"
:ok-title="this.$t('Merge')" :ok-title="this.$t('Merge')"
:cancel-title="this.$t('Cancel')" :cancel-title="this.$t('Cancel')"
@ok="mergeKeyword(this_item.id, this_item.target.id)"> @ok="mergeFood(this_item.id, this_item.target.id)">
{{ this.$t("merge_selection", {'source': this_item.name, 'type': this.$t('keyword')}) }} {{ this.$t("merge_selection", {'source': this_item.name, 'type': this.$t('food')}) }}
<generic-multiselect <generic-multiselect
@change="this_item.target=$event.val" @change="this_item.target=$event.val"
label="name" label="name"
search_function="listKeywords" search_function="listFoods"
:multiple="false" :multiple="false"
:tree_api="true" :tree_api="true"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0" style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
@ -193,36 +198,19 @@ import {BootstrapVue} from 'bootstrap-vue'
import 'bootstrap-vue/dist/bootstrap-vue.css' import 'bootstrap-vue/dist/bootstrap-vue.css'
import _debounce from 'lodash/debounce' import _debounce from 'lodash/debounce'
import {ResolveUrlMixin} from "@/utils/utils"; import {ToastMixin} from "@/utils/utils";
import {ApiApiFactory} from "@/utils/openapi/api.ts"; import {ApiApiFactory} from "@/utils/openapi/api.ts";
import FoodCard from "@/components/FoodCard"; import FoodCard from "@/components/FoodCard";
import GenericMultiselect from "@/components/GenericMultiselect"; import GenericMultiselect from "@/components/GenericMultiselect";
import InfiniteLoading from 'vue-infinite-loading'; import InfiniteLoading from 'vue-infinite-loading';
// would move with modals if made generic
import {TwemojiTextarea} from '@kevinfaguiar/vue-twemoji-picker';
// TODO add localization
import EmojiAllData from '@kevinfaguiar/vue-twemoji-picker/emoji-data/en/emoji-all-groups.json';
import EmojiGroups from '@kevinfaguiar/vue-twemoji-picker/emoji-data/emoji-groups.json';
// end move with generic modals
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
export default { export default {
name: 'FoodListView', name: 'FoodListView',
mixins: [ResolveUrlMixin], mixins: [ToastMixin],
components: {TwemojiTextarea, FoodCard, GenericMultiselect, InfiniteLoading}, components: {FoodCard, GenericMultiselect, InfiniteLoading},
computed: {
// move with generic modals
emojiDataAll() {
return EmojiAllData;
},
emojiGroups() {
return EmojiGroups;
}
// end move with generic modals
},
data() { data() {
return { return {
foods: [], foods: [],
@ -241,7 +229,9 @@ export default {
'id': -1, 'id': -1,
'name': '', 'name': '',
'description': '', 'description': '',
'icon': '', 'recipe': null,
'ignore_shopping': '',
'supermarket_category': null,
'target': { 'target': {
'id': -1, 'id': -1,
'name': '' 'name': ''
@ -262,15 +252,6 @@ export default {
}, 700) }, 700)
}, },
methods: { methods: {
makeToast: function (title, message, variant = null) {
//TODO remove duplicate function in favor of central one
this.$bvToast.toast(message, {
title: title,
variant: variant,
toaster: 'b-toaster-top-center',
solid: true
})
},
resetSearch: function () { resetSearch: function () {
if (this.search_input !== '') { if (this.search_input !== '') {
this.search_input = '' this.search_input = ''
@ -295,26 +276,26 @@ export default {
if (e.action == 'delete') { if (e.action == 'delete') {
this.this_item = source this.this_item = source
this.$bvModal.show('id_modal_keyword_delete') this.$bvModal.show('id_modal_food_delete')
} else if (e.action == 'new') { } else if (e.action == 'new') {
this.this_item = {} this.this_item = {}
this.$bvModal.show('id_modal_keyword_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
this.$bvModal.show('id_modal_keyword_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
if (target == null) { if (target == null) {
this.$bvModal.show('id_modal_keyword_move') this.$bvModal.show('id_modal_food_move')
} else { } else {
this.moveKeyword(source.id, target.id) this.moveFood(source.id, target.id)
} }
} else if (e.action === 'merge') { } else if (e.action === 'merge') {
this.this_item = source this.this_item = source
if (target == null) { if (target == null) {
this.$bvModal.show('id_modal_keyword_merge') this.$bvModal.show('id_modal_food_merge')
} else { } else {
this.mergeKeyword(e.source.id, e.target.id) this.mergeFood(e.source.id, e.target.id)
} }
} else if (e.action === 'get-children') { } else if (e.action === 'get-children') {
if (source.expanded) { if (source.expanded) {
@ -335,13 +316,14 @@ export default {
}, },
saveFood: function () { saveFood: function () {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
let food = { console.log(this.this_item, !this.this_item.id)
name: this.this_item.name, // let food = {
description: this.this_item.description, // name: this.this_item.name,
icon: this.this_item.icon, // description: this.this_item.description,
} // icon: this.this_item.icon,
// }
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(food).then(result => { apiClient.createFood(this.this_item).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
@ -356,7 +338,7 @@ export default {
this.this_item = {} this.this_item = {}
}) })
} else { } else {
apiClient.partialUpdateFood(this.this_item.id, food).then(result => { apiClient.partialUpdateFood(this.this_item.id, this.this_item).then(result => {
this.refreshCard(this.this_item.id) this.refreshCard(this.this_item.id)
this.this_item={} this.this_item={}
}).catch((err) => { }).catch((err) => {
@ -427,9 +409,9 @@ export default {
apiClient.listFoods(query, root, tree, page, pageSize).then(result => { apiClient.listFoods(query, root, tree, page, pageSize).then(result => {
if (col == 'left') { if (col == 'left') {
parent = this.findFood(this.keywords, food.id) parent = this.findFood(this.foods, food.id)
} else if (col == 'right'){ } else if (col == 'right'){
parent = this.findFood(this.keywords2, food.id) parent = this.findFood(this.foods2, food.id)
} }
if (parent) { if (parent) {
Vue.set(parent, 'children', result.data.results) Vue.set(parent, 'children', result.data.results)

View File

@ -194,7 +194,7 @@ import {BootstrapVue} from 'bootstrap-vue'
import 'bootstrap-vue/dist/bootstrap-vue.css' import 'bootstrap-vue/dist/bootstrap-vue.css'
import _debounce from 'lodash/debounce' import _debounce from 'lodash/debounce'
import {ResolveUrlMixin} from "@/utils/utils"; import {ToastMixin} from "@/utils/utils";
import {ApiApiFactory} from "@/utils/openapi/api.ts"; import {ApiApiFactory} from "@/utils/openapi/api.ts";
import KeywordCard from "@/components/KeywordCard"; import KeywordCard from "@/components/KeywordCard";
@ -212,7 +212,7 @@ Vue.use(BootstrapVue)
export default { export default {
name: 'KeywordListView', name: 'KeywordListView',
mixins: [ResolveUrlMixin], mixins: [ToastMixin],
components: {TwemojiTextarea, KeywordCard, GenericMultiselect, InfiniteLoading}, components: {TwemojiTextarea, KeywordCard, GenericMultiselect, InfiniteLoading},
computed: { computed: {
// move with generic modals // move with generic modals
@ -263,15 +263,6 @@ export default {
}, 700) }, 700)
}, },
methods: { methods: {
makeToast: function (title, message, variant = null) {
//TODO remove duplicate function in favor of central one
this.$bvToast.toast(message, {
title: title,
variant: variant,
toaster: 'b-toaster-top-center',
solid: true
})
},
resetSearch: function () { resetSearch: function () {
if (this.search_input !== '') { if (this.search_input !== '') {
this.search_input = '' this.search_input = ''

View File

@ -65,6 +65,11 @@ export default {
apiClient[this.search_function](query, root, tree, page, pageSize).then(result => { apiClient[this.search_function](query, root, tree, page, pageSize).then(result => {
this.objects = this.sticky_options.concat(result.data.results) this.objects = this.sticky_options.concat(result.data.results)
}) })
} else if (this.search_function === 'listRecipes') {
apiClient[this.search_function](query, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, 25, undefined).then(result => {
this.objects = this.sticky_options.concat(result.data.results)
})
} else { } else {
apiClient[this.search_function]({query: {query: query, limit: this.limit}}).then(result => { apiClient[this.search_function]({query: {query: query, limit: this.limit}}).then(result => {
this.objects = this.sticky_options.concat(result.data) this.objects = this.sticky_options.concat(result.data)

View File

@ -102,5 +102,8 @@
"merge_selection": "Replace all occurences of {source} with the selected {type}.", "merge_selection": "Replace all occurences of {source} with the selected {type}.",
"Advanced Search Settings": "Advanced Search Settings", "Advanced Search Settings": "Advanced Search Settings",
"Download": "Download", "Download": "Download",
"Root": "Root" "Root": "Root",
"Ignore_Shopping": "Ignore Shopping",
"Shopping_Category": "Shopping Category",
"Edit_Food": "Edit Food"
} }