hierarchical keyword filtering in recipe search

This commit is contained in:
smilerz 2021-08-13 09:58:02 -05:00
parent 1f21631c5a
commit 170673f467
38 changed files with 335 additions and 155 deletions

View File

@ -1,5 +1,7 @@
import django_filters
from django.conf import settings
from django.contrib.postgres.search import TrigramSimilarity
from django.db.models import Q
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled

View File

@ -32,7 +32,7 @@ def rescale_image_png(image_object, base_width=720):
def get_filetype(name):
try:
return os.path.splitext(name)[1]
except:
except Exception:
return '.jpeg'

View File

@ -3,8 +3,6 @@ Source: https://djangosnippets.org/snippets/1703/
"""
from django.conf import settings
from django.core.cache import caches
from django.views.generic.detail import SingleObjectTemplateResponseMixin
from django.views.generic.edit import ModelFormMixin
from cookbook.models import ShareLink
from django.contrib import messages
@ -64,7 +62,7 @@ def is_object_owner(user, obj):
return False
try:
return obj.get_owner() == user
except:
except Exception:
return False

View File

@ -4,7 +4,7 @@ from recipes import settings
from django.contrib.postgres.search import (
SearchQuery, SearchRank, TrigramSimilarity
)
from django.db.models import Max, Q, Subquery, Case, When, Value
from django.db.models import Count, Q, Subquery, Case, When, Value
from django.utils import translation
from cookbook.managers import DICTIONARY
@ -14,6 +14,7 @@ from cookbook.models import Food, Keyword, ViewLog
def search_recipes(request, queryset, params):
search_prefs = request.user.searchpreference
search_string = params.get('query', '')
search_ratings = params.getlist('ratings', [])
search_keywords = params.getlist('keywords', [])
search_foods = params.getlist('foods', [])
search_books = params.getlist('books', [])
@ -27,6 +28,7 @@ def search_recipes(request, queryset, params):
search_new = params.get('new', False)
search_last_viewed = int(params.get('last_viewed', 0))
# TODO update this to concat with full search queryset qs1 | qs2
if search_last_viewed > 0:
last_viewed_recipes = ViewLog.objects.filter(
created_by=request.user, space=request.space,
@ -122,11 +124,18 @@ def search_recipes(request, queryset, params):
queryset = queryset.filter(query_filter)
if len(search_keywords) > 0:
# TODO creating setting to include descendants of keywords a setting
if search_keywords_or == 'true':
# when performing an 'or' search all descendants are included in the OR condition
# so descendants are appended to
for kw in Keyword.objects.filter(pk__in=search_keywords):
search_keywords += list(kw.get_descendants().values_list('pk', flat=True))
queryset = queryset.filter(keywords__id__in=search_keywords)
else:
for k in search_keywords:
queryset = queryset.filter(keywords__id=k)
# when performing an 'and' search returned recipes should include a parent OR any of its descedants
# AND other keywords selected so filters are appended using keyword__id__in the list of keywords and descendants
for kw in Keyword.objects.filter(pk__in=search_keywords):
queryset = queryset.filter(keywords__id__in=list(kw.get_descendants_and_self().values_list('pk', flat=True)))
if len(search_foods) > 0:
if search_foods_or == 'true':
@ -153,9 +162,118 @@ def search_recipes(request, queryset, params):
# TODO add order by user settings
orderby += ['name']
queryset = queryset.order_by(*orderby)
return queryset
def get_facet(qs, params):
facets = {}
ratings = params.getlist('ratings', [])
keyword_list = params.getlist('keywords', [])
ingredient_list = params.getlist('ingredient', [])
book_list = params.getlist('book', [])
# this returns a list of keywords in the queryset and how many times it appears
# Keyword.objects.filter(recipe__in=queryset).annotate(kw_count=Count('recipe'))
kws = Keyword.objects.filter(recipe__in=qs).annotate(kw_count=Count('recipe'))
# custom django-tree function annotates a queryset to make building a tree easier.
# see https://django-treebeard.readthedocs.io/en/latest/api.html#treebeard.models.Node.get_annotated_list_qs for details
kw_a = annotated_qs(kws, root=True, fill=True)
# TODO add rating facet
facets['Ratings'] = []
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
# TODO add food facet
facets['Ingredients'] = []
# TODO add book facet
facets['Books'] = []
return facets
def fill_annotated_parents(annotation, filters):
tree_list = []
parent = []
i = 0
level = -1
for r in annotation:
expand = False
annotation[i][1]['id'] = r[0].id
annotation[i][1]['name'] = r[0].name
annotation[i][1]['count'] = getattr(r[0], 'kw_count', 0)
annotation[i][1]['isDefaultExpanded'] = False
if str(r[0].id) in filters:
expand = True
if r[1]['level'] < level:
parent = parent[:r[1]['level'] - level]
parent[-1] = i
level = r[1]['level']
elif r[1]['level'] > level:
parent.extend([i])
level = r[1]['level']
else:
parent[-1] = i
j = 0
while j < level:
# this causes some double counting when a recipe has both a child and an ancestor
annotation[parent[j]][1]['count'] += getattr(r[0], 'kw_count', 0)
if expand:
annotation[parent[j]][1]['isDefaultExpanded'] = True
j += 1
if level == 0:
tree_list.append(annotation[i][1])
elif level > 0:
annotation[parent[level - 1]][1].setdefault('children', []).append(annotation[i][1])
i += 1
return tree_list
def annotated_qs(qs, root=False, fill=False):
"""
Gets an annotated list from a queryset.
:param root:
Will backfill in annotation to include all parents to root node.
:param fill:
Will fill in gaps in annotation where nodes between children
and ancestors are not included in the queryset.
"""
result, info = [], {}
start_depth, prev_depth = (None, None)
nodes_list = list(qs.values_list('pk', flat=True))
for node in qs.order_by('path'):
node_queue = [node]
while len(node_queue) > 0:
dirty = False
current_node = node_queue[-1]
depth = current_node.get_depth()
parent_id = current_node.parent
if root and depth > 1 and parent_id not in nodes_list:
parent_id = current_node.parent
nodes_list.append(parent_id)
node_queue.append(current_node.__class__.objects.get(pk=parent_id))
dirty = True
if fill and depth > 1 and prev_depth and depth > prev_depth and parent_id not in nodes_list:
nodes_list.append(parent_id)
node_queue.append(current_node.__class__.objects.get(pk=parent_id))
dirty = True
if not dirty:
working_node = node_queue.pop()
if start_depth is None:
start_depth = depth
open = (depth and (prev_depth is None or depth > prev_depth))
if prev_depth is not None and depth < prev_depth:
info['close'] = list(range(0, prev_depth - depth))
info = {'open': open, 'close': [], 'level': depth - start_depth}
result.append((working_node, info,))
prev_depth = depth
if start_depth and start_depth > 0:
info['close'] = list(range(0, prev_depth - start_depth + 1))
return result

View File

@ -1,4 +1,3 @@
from django.shortcuts import redirect
from django.urls import reverse
from django_scopes import scope, scopes_disabled

View File

@ -6,6 +6,7 @@ from cookbook.helper.mdx_urlize import UrlizeExtension
from jinja2 import Template, TemplateSyntaxError, UndefinedError
from gettext import gettext as _
class IngredientObject(object):
amount = ""
unit = ""

View File

@ -1,11 +1,6 @@
import json
import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
from cookbook.models import Recipe, Step, Ingredient
class Pepperplate(Integration):

View File

@ -1,10 +1,8 @@
import re
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
from cookbook.models import Recipe, Step, Ingredient
class ChefTap(Integration):

View File

@ -1,4 +1,3 @@
import json
import re
from io import BytesIO
from zipfile import ZipFile
@ -6,7 +5,7 @@ from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
from cookbook.models import Recipe, Step, Ingredient, Keyword
class Chowdown(Integration):

View File

@ -1,7 +1,5 @@
import datetime
import json
import os
import re
import traceback
import uuid
from io import BytesIO, StringIO

View File

@ -6,7 +6,7 @@ from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
from cookbook.models import Recipe, Step, Ingredient
class Mealie(Integration):
@ -50,7 +50,7 @@ class Mealie(Integration):
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
except:
except Exception:
pass
recipe.steps.add(step)
@ -59,7 +59,7 @@ class Mealie(Integration):
import_zip = ZipFile(f['file'])
try:
self.import_recipe_image(recipe, BytesIO(import_zip.read(f'recipes/{recipe_json["slug"]}/images/min-original.webp')), filetype=get_filetype(f'recipes/{recipe_json["slug"]}/images/original'))
except:
except Exception:
pass
return recipe

View File

@ -1,11 +1,8 @@
import json
import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
from cookbook.models import Recipe, Step, Ingredient, Keyword
class MealMaster(Integration):

View File

@ -6,7 +6,7 @@ from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
from cookbook.models import Recipe, Step, Ingredient
class NextcloudCookbook(Integration):

View File

@ -1,11 +1,8 @@
import json
import re
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
from cookbook.models import Recipe, Step, Ingredient
class OpenEats(Integration):

View File

@ -1,16 +1,14 @@
import re
import json
import base64
import requests
from io import BytesIO
from zipfile import ZipFile
import imghdr
from django.utils.translation import gettext as _
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
from cookbook.models import Recipe, Step, Ingredient, Keyword
class RecetteTek(Integration):
@ -108,7 +106,7 @@ class RecetteTek(Integration):
recipe.keywords.add(k)
recipe.save()
except Exception as e:
pass
print(recipe.name, ': failed to parse keywords ', str(e))
# TODO: Parse Nutritional Information
@ -123,7 +121,7 @@ class RecetteTek(Integration):
else:
if file['originalPicture'] != '':
response = requests.get(file['originalPicture'])
if imghdr.what(BytesIO(response.content)) != None:
if imghdr.what(BytesIO(response.content)) is not None:
self.import_recipe_image(recipe, BytesIO(response.content), filetype=get_filetype(file['originalPicture']))
else:
raise Exception("Original image failed to download.")

View File

@ -3,12 +3,10 @@ from bs4 import BeautifulSoup
from io import BytesIO
from zipfile import ZipFile
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.recipe_url_import import parse_servings, iso_duration_to_minutes
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
from cookbook.models import Recipe, Step, Ingredient, Keyword
class RecipeKeeper(Integration):
@ -61,7 +59,6 @@ class RecipeKeeper(Integration):
if file.find("span", {"itemprop": "recipeSource"}).text != '':
step.instruction += "\n\nImported from: " + file.find("span", {"itemprop": "recipeSource"}).text
step.save()
source_url_added = True
recipe.steps.add(step)
@ -72,7 +69,7 @@ class RecipeKeeper(Integration):
import_zip = ZipFile(f['file'])
self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipe-photo").get("src"))), filetype='.jpeg')
except Exception as e:
pass
print(recipe.name, ': failed to import image ', str(e))
return recipe

View File

@ -1,9 +1,7 @@
import base64
import json
from io import BytesIO
import requests
from rest_framework.renderers import JSONRenderer
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration

View File

@ -1,11 +1,6 @@
import json
import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
from cookbook.models import Recipe, Step, Ingredient, Keyword
class RezKonv(Integration):

View File

@ -2,7 +2,7 @@ from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
from cookbook.models import Recipe, Step, Ingredient
class Safron(Integration):

View File

@ -27,5 +27,5 @@ class Command(BaseCommand):
Step.objects.all().update(search_vector=SearchVector('instruction__unaccent', weight='B', config=language))
self.stdout.write(self.style.SUCCESS(_('Recipe index rebuild complete.')))
except:
except Exception:
self.stdout.write(self.style.ERROR(_('Recipe index rebuild failed.')))

View File

@ -1,6 +1,6 @@
from django.contrib.postgres.aggregates import StringAgg
from django.contrib.postgres.search import (
SearchQuery, SearchRank, SearchVector, TrigramSimilarity,
SearchQuery, SearchRank, SearchVector,
)
from django.db import models
from django.db.models import Q

View File

@ -5,7 +5,6 @@ import uuid
from datetime import date, timedelta
from annoying.fields import AutoOneToOneField
from django.conf import settings
from django.contrib import auth
from django.contrib.auth.models import Group, User
from django.contrib.postgres.indexes import GinIndex
@ -19,7 +18,7 @@ from django.utils.translation import gettext as _
from treebeard.mp_tree import MP_Node, MP_NodeManager
from django_scopes import ScopedManager, scopes_disabled
from django_prometheus.models import ExportModelOperationsMixin
from recipes.settings import (COMMENT_PREF_DEFAULT, DATABASES, FRACTION_PREF_DEFAULT,
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT,
STICKY_NAV_PREF_DEFAULT)
@ -277,6 +276,7 @@ class SyncLog(models.Model, PermissionModelMixin):
class Keyword(ExportModelOperationsMixin('keyword'), MP_Node, PermissionModelMixin):
# TODO add find and fix problem functions
node_order_by = ['name']
name = models.CharField(max_length=64)
icon = models.CharField(max_length=16, blank=True, null=True)

View File

@ -1,6 +1,5 @@
import io
import os
import tempfile
from datetime import datetime
from os import listdir
from os.path import isfile, join

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

@ -9,6 +9,10 @@ from cookbook.models import Food, Ingredient, Step, Recipe
LIST_URL = 'api:recipe-list'
DETAIL_URL = 'api:recipe-detail'
# TODO need to add extensive tests against recipe search to go through all of the combinations of parameters
# probably needs to include a far more extensive set of initial recipes to effectively test results
# and to ensure that all parts of the code are exercised.
# TODO should probably consider adding code coverage plugin to the test suite
@pytest.mark.parametrize("arg", [
['a_u', 403],

View File

@ -4,9 +4,9 @@ import re
import uuid
import requests
from PIL import Image
from annoying.decorators import ajax_request
from annoying.functions import get_object_or_None
from collections import OrderedDict
from django.contrib import messages
from django.contrib.auth.models import User
from django.contrib.postgres.search import TrigramSimilarity
@ -37,7 +37,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
group_required)
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_search import search_recipes
from cookbook.helper.recipe_search import search_recipes, get_facet
from cookbook.helper.recipe_url_import import get_from_scraper
from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
MealType, Recipe, RecipeBook, ShoppingList,
@ -326,6 +326,7 @@ class SupermarketCategoryRelationViewSet(viewsets.ModelViewSet, StandardFilterMi
queryset = SupermarketCategoryRelation.objects
serializer_class = SupermarketCategoryRelationSerializer
permission_classes = [CustomIsUser]
pagination_class = DefaultPagination
def get_queryset(self):
self.queryset = self.queryset.filter(supermarket__space=self.request.space)
@ -446,6 +447,19 @@ class RecipePagination(PageNumberPagination):
page_size_query_param = 'page_size'
max_page_size = 100
def paginate_queryset(self, queryset, request, view=None):
self.facets = get_facet(queryset, request.query_params)
return super().paginate_queryset(queryset, request, view)
def get_paginated_response(self, data):
return Response(OrderedDict([
('count', self.page.paginator.count),
('next', self.get_next_link()),
('previous', self.get_previous_link()),
('results', data),
('facets', self.facets)
]))
class RecipeViewSet(viewsets.ModelViewSet):
queryset = Recipe.objects

View File

@ -13,7 +13,7 @@ from django.urls import reverse
from django.utils.translation import gettext as _
from django.utils.translation import ngettext
from django_tables2 import RequestConfig
from PIL import Image, UnidentifiedImageError
from PIL import UnidentifiedImageError
from requests.exceptions import MissingSchema
from cookbook.forms import BatchEditForm, SyncForm

View File

@ -3,12 +3,12 @@ import json
import requests
from django.db.models import Q
from django.http import JsonResponse
from django.shortcuts import render, get_object_or_404
from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from cookbook.helper.ingredient_parser import parse, get_unit, get_food
from cookbook.helper.permission_helper import group_required
from cookbook.models import TelegramBot, ShoppingList, ShoppingListEntry, Food, Unit
from cookbook.models import TelegramBot, ShoppingList, ShoppingListEntry
@group_required('user')
@ -58,7 +58,7 @@ def hook(request, token):
)
)
return JsonResponse({'data': data['message']['text']})
except:
except Exception:
pass
return JsonResponse({})

View File

@ -25,7 +25,7 @@ from cookbook.filters import RecipeFilter
from cookbook.forms import (CommentForm, Recipe, User,
UserCreateForm, UserNameForm, UserPreference,
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm,
SearchPreferenceForm, AllAuthSignupForm)
SearchPreferenceForm)
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,

View File

@ -15,11 +15,9 @@ import os
import re
from django.contrib import messages
from django.contrib.staticfiles.storage import staticfiles_storage
from django.utils.translation import gettext_lazy as _
from dotenv import load_dotenv
from webpack_loader.loader import WebpackLoader
load_dotenv()
# from dotenv import load_dotenv
# load_dotenv()
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Get vars from .env files

View File

@ -10,6 +10,7 @@
"dependencies": {
"@babel/eslint-parser": "^7.13.14",
"@kevinfaguiar/vue-twemoji-picker": "^5.7.4",
"@riophae/vue-treeselect": "^0.4.0",
"axios": "^0.21.1",
"bootstrap-vue": "^2.21.2",
"core-js": "^3.14.0",

View File

@ -10,16 +10,20 @@
<b-input class="form-control form-control-lg form-control-borderless form-control-search" v-model="settings.search_input"
v-bind:placeholder="$t('Search')"></b-input>
<b-input-group-append>
<b-button class="shadow-none btn btn-light" @click="openRandom()">
<b-button variant="light"
v-b-tooltip.hover :title="$t('Random Recipes')"
@click="openRandom()">
<i class="fas fa-dice-five" style="font-size: 1.5em"></i>
</b-button>
<b-button v-b-toggle.collapse_advanced_search
v-bind:class="{'btn-primary': !isAdvancedSettingsSet(), 'btn-danger': isAdvancedSettingsSet()}"
class="shadow-none btn"><i
class="fas fa-caret-down" v-if="!settings.advanced_search_visible"></i><i
class="fas fa-caret-up"
v-if="settings.advanced_search_visible"></i>
v-b-tooltip.hover :title="$t('Advanced Settings')"
v-bind:variant="!isAdvancedSettingsSet() ? 'primary' : 'danger'"
>
<!-- consider changing this icon to a filter -->
<i class="fas fa-caret-down" v-if="!settings.advanced_search_visible"></i>
<i class="fas fa-caret-up" v-if="settings.advanced_search_visible"></i>
</b-button>
</b-input-group-append>
</b-input-group>
</div>
@ -134,12 +138,16 @@
<div class="row">
<div class="col-12">
<b-input-group class="mt-2">
<generic-multiselect @change="genericSelectChanged" parent_variable="search_keywords"
<!-- <generic-multiselect @change="genericSelectChanged" parent_variable="search_keywords"
:initial_selection="settings.search_keywords"
search_function="listKeywords" label="label"
:tree_api="true"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Keywords')"></generic-multiselect>
v-bind:placeholder="$t('Keywords')"></generic-multiselect> -->
<treeselect v-model="settings.search_keywords" :options="facets.Keywords" :flat="true"
searchNested multiple :placeholder="$t('Keywords')" :normalizer="normalizer"
@input="refreshData(false)"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"/>
<b-input-group-append>
<b-input-group-text>
<b-form-checkbox v-model="settings.search_keywords_or" name="check-button"
@ -254,8 +262,6 @@
</div>
</div>
</div>
</template>
<script>
@ -277,6 +283,8 @@ import LoadingSpinner from "@/components/LoadingSpinner";
import {ApiApiFactory} from "@/utils/openapi/api.ts";
import RecipeCard from "@/components/RecipeCard";
import GenericMultiselect from "@/components/GenericMultiselect";
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
Vue.use(BootstrapVue)
@ -285,10 +293,11 @@ let SETTINGS_COOKIE_NAME = 'search_settings'
export default {
name: 'RecipeSearchView',
mixins: [ResolveUrlMixin],
components: {GenericMultiselect, RecipeCard},
components: {GenericMultiselect, RecipeCard, Treeselect},
data() {
return {
recipes: [],
facets: [],
meal_plans: [],
last_viewed_recipes: [],
@ -383,9 +392,7 @@ export default {
apiClient.listRecipes(
this.settings.search_input,
this.settings.search_keywords.map(function (A) {
return A["id"];
}),
this.settings.search_keywords,
this.settings.search_foods.map(function (A) {
return A["id"];
}),
@ -405,6 +412,7 @@ export default {
window.scrollTo(0, 0);
this.pagination_count = result.data.count
this.recipes = result.data.results
this.facets = result.data.facets
})
},
openRandom: function () {
@ -458,6 +466,14 @@ export default {
},
isAdvancedSettingsSet() {
return ((this.settings.search_keywords.length + this.settings.search_foods.length + this.settings.search_books.length) > 0)
},
normalizer(node) {
return {
id: node.id,
label: node.name + ' (' + node.count + ')',
children: node.children,
isDefaultExpanded: node.isDefaultExpanded
}
}
}
}

View File

@ -79,7 +79,10 @@ module.exports = {
priority: 1
},
},
});
},
// 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
/*

View File

@ -1063,6 +1063,20 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
"@riophae/vue-treeselect@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@riophae/vue-treeselect/-/vue-treeselect-0.4.0.tgz#0baed5a794cffc580b63591f35c125e51c0df241"
integrity sha512-J4atYmBqXQmiPFK/0B5sXKjtnGc21mBJEiyKIDZwk0Q9XuynVFX6IJ4EpaLmUgL5Tve7HAS7wkiGGSti6Uaxcg==
dependencies:
"@babel/runtime" "^7.3.1"
babel-helper-vue-jsx-merge-props "^2.0.3"
easings-css "^1.0.0"
fuzzysearch "^1.0.3"
is-promise "^2.1.0"
lodash "^4.0.0"
material-colors "^1.2.6"
watch-size "^2.0.0"
"@rollup/plugin-babel@^5.2.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879"
@ -2255,6 +2269,11 @@ babel-extract-comments@^1.0.0:
dependencies:
babylon "^6.18.0"
babel-helper-vue-jsx-merge-props@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz#22aebd3b33902328e513293a8e4992b384f9f1b6"
integrity sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg==
babel-loader@^8.1.0:
version "8.2.2"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.2.tgz#9363ce84c10c9a40e6c753748e1441b60c8a0b81"
@ -3804,6 +3823,11 @@ duplexify@^3.4.2, duplexify@^3.6.0:
readable-stream "^2.0.0"
stream-shift "^1.0.0"
easings-css@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/easings-css/-/easings-css-1.0.0.tgz#dde569003bb7a4a0c0b77878f5db3e0be5679c81"
integrity sha512-7Uq7NdazNfVtr0RNmPAys8it0zKCuaqxJStYKEl72D3j4gbvXhhaM7iWNbqhA4C94ygCye6VuyhzBRQC4szeBg==
easy-stack@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/easy-stack/-/easy-stack-1.0.1.tgz#8afe4264626988cabb11f3c704ccd0c835411066"
@ -4674,6 +4698,11 @@ functional-red-black-tree@^1.0.1:
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
fuzzysearch@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/fuzzysearch/-/fuzzysearch-1.0.3.tgz#dffc80f6d6b04223f2226aa79dd194231096d008"
integrity sha1-3/yA9tawQiPyImqnndGUIxCW0Ag=
generic-names@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-2.0.1.tgz#f8a378ead2ccaa7a34f0317b05554832ae41b872"
@ -5579,6 +5608,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-promise@^2.1.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
is-regex@^1.0.4, is-regex@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
@ -6052,7 +6086,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3:
lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -6137,6 +6171,11 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
material-colors@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@ -9509,6 +9548,11 @@ vuex@^3.6.0:
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
integrity sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==
watch-size@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/watch-size/-/watch-size-2.0.0.tgz#096ee28d0365bd7ea03d9c8bf1f2f50a73be1474"
integrity sha512-M92R89dNoTPWyCD+HuUEDdhaDnh9jxPGOwlDc0u51jAgmjUvzqaEMynXSr3BaWs+QdHYk4KzibPy1TFtjLmOZQ==
watchpack-chokidar2@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"