trees in recipe search loaded asynchronously
This commit is contained in:
parent
20d61160ba
commit
22953b0591
@ -201,12 +201,11 @@ def search_recipes(request, queryset, params):
|
||||
return queryset
|
||||
|
||||
|
||||
class CacheEmpty(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RecipeFacet():
|
||||
def __init__(self, request, queryset=None, hash_key=None, cache_timeout=600):
|
||||
class CacheEmpty(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, request, queryset=None, hash_key=None, cache_timeout=3600):
|
||||
if hash_key is None and queryset is None:
|
||||
raise ValueError(_("One of queryset or hash_key must be provided"))
|
||||
|
||||
@ -215,15 +214,15 @@ class RecipeFacet():
|
||||
self.hash_key = hash_key or str(hash(frozenset(self._queryset.values_list('pk'))))
|
||||
self._SEARCH_CACHE_KEY = f"recipes_filter_{self.hash_key}"
|
||||
self._cache_timeout = cache_timeout
|
||||
self._cache = caches['default'].get(self._SEARCH_CACHE_KEY, None)
|
||||
self._cache = caches['default'].get(self._SEARCH_CACHE_KEY, {})
|
||||
if self._cache is None and self._queryset is None:
|
||||
raise CacheEmpty("No queryset provided and cache empty")
|
||||
raise self.CacheEmpty("No queryset provided and cache empty")
|
||||
|
||||
self.Keywords = getattr(self._cache, 'Keywords', None)
|
||||
self.Foods = getattr(self._cache, 'Foods', None)
|
||||
self.Books = getattr(self._cache, 'Books', None)
|
||||
self.Ratings = getattr(self._cache, 'Ratings', None)
|
||||
self.Recent = getattr(self._cache, 'Recent', None)
|
||||
self.Keywords = self._cache.get('Keywords', None)
|
||||
self.Foods = self._cache.get('Foods', None)
|
||||
self.Books = self._cache.get('Books', None)
|
||||
self.Ratings = self._cache.get('Ratings', None)
|
||||
self.Recent = self._cache.get('Recent', None)
|
||||
|
||||
if self._queryset:
|
||||
self._recipe_list = list(self._queryset.values_list('id', flat=True))
|
||||
@ -292,16 +291,9 @@ class RecipeFacet():
|
||||
else:
|
||||
keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(depth=1)).filter(space=self._request.space).distinct()
|
||||
|
||||
# Subquery that counts recipes for keyword including children
|
||||
kw_recipe_count = Recipe.objects.filter(**{'keywords__path__startswith': OuterRef('path')}, id__in=self._recipe_list, space=self._request.space
|
||||
).values(kw=Substr('keywords__path', 1, Keyword.steplen)
|
||||
).annotate(count=Count('pk', distinct=True)).values('count')
|
||||
|
||||
# set keywords to root objects only
|
||||
keywords = keywords.annotate(count=Coalesce(Subquery(kw_recipe_count), 0)
|
||||
).filter(depth=1, count__gt=0
|
||||
).values('id', 'name', 'count', 'numchild').order_by('name')
|
||||
self.Keywords = list(keywords)
|
||||
keywords = self._keyword_queryset(keywords)
|
||||
self.Keywords = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
self.set_cache('Keywords', self.Keywords)
|
||||
return self.Keywords
|
||||
|
||||
@ -313,16 +305,10 @@ class RecipeFacet():
|
||||
else:
|
||||
foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(depth=1)).filter(space=self._request.space).distinct()
|
||||
|
||||
food_recipe_count = Recipe.objects.filter(**{'steps__ingredients__food__path__startswith': OuterRef('path')}, id__in=self._recipe_list, space=self._request.space
|
||||
).values(kw=Substr('steps__ingredients__food__path', 1, Food.steplen)
|
||||
).annotate(count=Count('pk', distinct=True)).values('count')
|
||||
|
||||
# set keywords to root objects only
|
||||
foods = foods.annotate(count=Coalesce(Subquery(food_recipe_count), 0)
|
||||
).filter(depth=1, count__gt=0
|
||||
).values('id', 'name', 'count', 'numchild'
|
||||
).order_by('name')
|
||||
self.Foods = list(foods)
|
||||
foods = self._food_queryset(foods)
|
||||
|
||||
self.Foods = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
|
||||
self.set_cache('Foods', self.Foods)
|
||||
return self.Foods
|
||||
|
||||
@ -349,6 +335,59 @@ class RecipeFacet():
|
||||
self.set_cache('Recent', self.Recent)
|
||||
return self.Recent
|
||||
|
||||
def add_food_children(self, id):
|
||||
try:
|
||||
food = Food.objects.get(id=id)
|
||||
nodes = food.get_ancestors()
|
||||
except Food.DoesNotExist:
|
||||
return self.get_facets()
|
||||
foods = self._food_queryset(Food.objects.filter(path__startswith=food.path, depth=food.depth+1), food)
|
||||
deep_search = self.Foods
|
||||
for node in nodes:
|
||||
index = next((i for i, x in enumerate(deep_search) if x["id"] == node.id), None)
|
||||
deep_search = deep_search[index]['children']
|
||||
index = next((i for i, x in enumerate(deep_search) if x["id"] == food.id), None)
|
||||
deep_search[index]['children'] = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
|
||||
self.set_cache('Foods', self.Foods)
|
||||
return self.get_facets()
|
||||
|
||||
def add_keyword_children(self, id):
|
||||
try:
|
||||
keyword = Keyword.objects.get(id=id)
|
||||
nodes = keyword.get_ancestors()
|
||||
except Keyword.DoesNotExist:
|
||||
return self.get_facets()
|
||||
keywords = self._keyword_queryset(Keyword.objects.filter(path__startswith=keyword.path, depth=keyword.depth+1), keyword)
|
||||
deep_search = self.Keywords
|
||||
for node in nodes:
|
||||
index = next((i for i, x in enumerate(deep_search) if x["id"] == node.id), None)
|
||||
deep_search = deep_search[index]['children']
|
||||
index = next((i for i, x in enumerate(deep_search) if x["id"] == keyword.id), None)
|
||||
deep_search[index]['children'] = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
self.set_cache('Keywords', self.Keywords)
|
||||
return self.get_facets()
|
||||
|
||||
def _recipe_count_queryset(self, field, depth=1, steplen=4):
|
||||
return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path')}, id__in=self._recipe_list, space=self._request.space
|
||||
).values(child=Substr(f'{field}__path', 1, steplen)
|
||||
).annotate(count=Count('pk', distinct=True)).values('count')
|
||||
|
||||
def _keyword_queryset(self, queryset, keyword=None):
|
||||
depth = getattr(keyword, 'depth', 0) + 1
|
||||
steplen = depth * Keyword.steplen
|
||||
|
||||
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('keywords', depth, steplen)), 0)
|
||||
).filter(depth=depth, count__gt=0
|
||||
).values('id', 'name', 'count', 'numchild').order_by('name')
|
||||
|
||||
def _food_queryset(self, queryset, food=None):
|
||||
depth = getattr(food, 'depth', 0) + 1
|
||||
steplen = depth * Food.steplen
|
||||
|
||||
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('steps__ingredients__food', depth, steplen)), 0)
|
||||
).filter(depth__lte=depth, count__gt=0
|
||||
).values('id', 'name', 'count', 'numchild').order_by('name')
|
||||
|
||||
|
||||
# # TODO: This might be faster https://github.com/django-treebeard/django-treebeard/issues/115
|
||||
# def get_facet(qs=None, request=None, use_cache=True, hash_key=None, food=None, keyword=None):
|
||||
|
@ -1135,9 +1135,16 @@ def get_facets(request):
|
||||
keyword = request.GET.get('keyword', None)
|
||||
facets = RecipeFacet(request, hash_key=key)
|
||||
|
||||
if food:
|
||||
results = facets.add_food_children(food)
|
||||
elif keyword:
|
||||
results = facets.add_keyword_children(keyword)
|
||||
else:
|
||||
results = facets.get_facets()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
'facets': facets.get_facets(),
|
||||
'facets': results,
|
||||
},
|
||||
status=200
|
||||
)
|
||||
|
@ -80,7 +80,7 @@
|
||||
</div>
|
||||
<div class="row" style="margin-top: 1vh">
|
||||
<div class="col-12">
|
||||
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{ $t("Advanced Search Settings") }}</a>
|
||||
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{ $t("Search Settings") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 1vh">
|
||||
@ -97,6 +97,7 @@
|
||||
<treeselect
|
||||
v-model="settings.search_keywords"
|
||||
:options="facets.Keywords"
|
||||
:load-options="loadKeywordChildren"
|
||||
:flat="true"
|
||||
searchNested
|
||||
:multiple="true"
|
||||
@ -123,7 +124,7 @@
|
||||
<b-input-group class="mt-2">
|
||||
<treeselect
|
||||
v-model="settings.search_foods"
|
||||
:options="foodFacet"
|
||||
:options="facets.Foods"
|
||||
:load-options="loadFoodChildren"
|
||||
:flat="true"
|
||||
searchNested
|
||||
@ -291,16 +292,6 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
foodFacet: function () {
|
||||
console.log("test", this.facets)
|
||||
return this.facets?.Foods?.map((x) => {
|
||||
if (x?.numchild > 0) {
|
||||
return { ...x, children: null }
|
||||
} else {
|
||||
return x
|
||||
}
|
||||
})
|
||||
},
|
||||
ratingOptions: function () {
|
||||
return [
|
||||
{ id: 5, label: "⭐⭐⭐⭐⭐" + " (" + (this.facets.Ratings?.["5.0"] ?? 0) + ")" },
|
||||
@ -414,10 +405,9 @@ export default {
|
||||
this.pagination_count = result.data.count
|
||||
|
||||
this.facets = result.data.facets
|
||||
console.log(this.facets)
|
||||
if (this.facets?.cache_key) {
|
||||
this.getFacets(this.facets.cache_key)
|
||||
}
|
||||
// if (this.facets?.cache_key) {
|
||||
// this.getFacets(this.facets.cache_key)
|
||||
// }
|
||||
this.recipes = this.removeDuplicates(result.data.results, (recipe) => recipe.id)
|
||||
if (!this.searchFiltered) {
|
||||
// if meal plans are being shown - filter out any meal plan recipes from the recipe list
|
||||
@ -491,8 +481,12 @@ export default {
|
||||
return [undefined, undefined]
|
||||
}
|
||||
},
|
||||
getFacets: function (hash) {
|
||||
return this.genericGetAPI("api_get_facets", { hash: hash }).then((response) => {
|
||||
getFacets: function (hash, facet, id) {
|
||||
let params = { hash: hash }
|
||||
if (facet) {
|
||||
params[facet] = id
|
||||
}
|
||||
return this.genericGetAPI("api_get_facets", params).then((response) => {
|
||||
this.facets = { ...this.facets, ...response.data.facets }
|
||||
})
|
||||
},
|
||||
@ -520,9 +514,7 @@ export default {
|
||||
} else {
|
||||
params.options = { query: { debug: true } }
|
||||
}
|
||||
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {
|
||||
console.log(result.data)
|
||||
})
|
||||
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {})
|
||||
},
|
||||
loadFoodChildren({ action, parentNode, callback }) {
|
||||
// Typically, do the AJAX stuff here.
|
||||
@ -530,28 +522,25 @@ export default {
|
||||
// assign children options to the parent node & call the callback.
|
||||
|
||||
if (action === LOAD_CHILDREN_OPTIONS) {
|
||||
switch (parentNode.id) {
|
||||
case "success": {
|
||||
console.log(parentNode)
|
||||
break
|
||||
}
|
||||
// case "no-children": {
|
||||
// simulateAsyncOperation(() => {
|
||||
// parentNode.children = []
|
||||
// callback()
|
||||
// })
|
||||
// break
|
||||
// }
|
||||
// case "failure": {
|
||||
// simulateAsyncOperation(() => {
|
||||
// callback(new Error("Failed to load options: network error."))
|
||||
// })
|
||||
// break
|
||||
// }
|
||||
default: /* empty */
|
||||
if (this.facets?.cache_key) {
|
||||
this.getFacets(this.facets.cache_key, "food", parentNode.id).then(callback())
|
||||
}
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
loadKeywordChildren({ action, parentNode, callback }) {
|
||||
// Typically, do the AJAX stuff here.
|
||||
// Once the server has responded,
|
||||
// assign children options to the parent node & call the callback.
|
||||
|
||||
if (action === LOAD_CHILDREN_OPTIONS) {
|
||||
if (this.facets?.cache_key) {
|
||||
this.getFacets(this.facets.cache_key, "keyword", parentNode.id).then(callback())
|
||||
}
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
callback()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -116,7 +116,7 @@
|
||||
"Information": "Information",
|
||||
"Download": "Download",
|
||||
"Create": "Create",
|
||||
"Advanced Search Settings": "Advanced Search Settings",
|
||||
"Search Settings": "Search Settings",
|
||||
"View": "View",
|
||||
"Recipes": "Recipes",
|
||||
"Move": "Move",
|
||||
|
Loading…
Reference in New Issue
Block a user