cached facet results
This commit is contained in:
parent
b7be5cd325
commit
b3cffa4a38
@ -1,6 +1,7 @@
|
|||||||
# only set this to true when testing/debugging
|
# only set this to true when testing/debugging
|
||||||
# when unset: 1 (true) - dont unset this, just for development
|
# when unset: 1 (true) - dont unset this, just for development
|
||||||
DEBUG=0
|
DEBUG=0
|
||||||
|
SQL_DEBUG=0
|
||||||
|
|
||||||
# hosts the application can run under e.g. recipes.mydomain.com,cooking.mydomain.com,...
|
# hosts the application can run under e.g. recipes.mydomain.com,cooking.mydomain.com,...
|
||||||
ALLOWED_HOSTS=*
|
ALLOWED_HOSTS=*
|
||||||
|
@ -5,6 +5,7 @@ from recipes import settings
|
|||||||
from django.contrib.postgres.search import (
|
from django.contrib.postgres.search import (
|
||||||
SearchQuery, SearchRank, TrigramSimilarity
|
SearchQuery, SearchRank, TrigramSimilarity
|
||||||
)
|
)
|
||||||
|
from django.core.cache import caches
|
||||||
from django.db.models import Avg, Case, Count, Func, Max, Q, Subquery, Value, When
|
from django.db.models import Avg, Case, Count, Func, Max, Q, Subquery, Value, When
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
|
|
||||||
@ -17,6 +18,13 @@ class Round(Func):
|
|||||||
template = '%(function)s(%(expressions)s, 0)'
|
template = '%(function)s(%(expressions)s, 0)'
|
||||||
|
|
||||||
|
|
||||||
|
def str2bool(v):
|
||||||
|
if type(v) == bool:
|
||||||
|
return v
|
||||||
|
else:
|
||||||
|
return v.lower() in ("yes", "true", "1")
|
||||||
|
|
||||||
|
|
||||||
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
|
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
|
||||||
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
|
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
|
||||||
def search_recipes(request, queryset, params):
|
def search_recipes(request, queryset, params):
|
||||||
@ -32,13 +40,13 @@ def search_recipes(request, queryset, params):
|
|||||||
search_units = params.get('units', None)
|
search_units = params.get('units', None)
|
||||||
|
|
||||||
# TODO I think default behavior should be 'AND' which is how most sites operate with facet/filters based on results
|
# TODO I think default behavior should be 'AND' which is how most sites operate with facet/filters based on results
|
||||||
search_keywords_or = params.get('keywords_or', True)
|
search_keywords_or = str2bool(params.get('keywords_or', True))
|
||||||
search_foods_or = params.get('foods_or', True)
|
search_foods_or = str2bool(params.get('foods_or', True))
|
||||||
search_books_or = params.get('books_or', True)
|
search_books_or = str2bool(params.get('books_or', True))
|
||||||
|
|
||||||
search_internal = params.get('internal', None)
|
search_internal = str2bool(params.get('internal', None))
|
||||||
search_random = params.get('random', False)
|
search_random = str2bool(params.get('random', False))
|
||||||
search_new = params.get('new', False)
|
search_new = str2bool(params.get('new', False))
|
||||||
search_last_viewed = int(params.get('last_viewed', 0))
|
search_last_viewed = int(params.get('last_viewed', 0))
|
||||||
orderby = []
|
orderby = []
|
||||||
|
|
||||||
@ -58,7 +66,7 @@ def search_recipes(request, queryset, params):
|
|||||||
# TODO create setting for default ordering - most cooked, rating,
|
# TODO create setting for default ordering - most cooked, rating,
|
||||||
# TODO create options for live sorting
|
# TODO create options for live sorting
|
||||||
# TODO make days of new recipe a setting
|
# TODO make days of new recipe a setting
|
||||||
if search_new == 'true':
|
if search_new:
|
||||||
queryset = (
|
queryset = (
|
||||||
queryset.annotate(new_recipe=Case(
|
queryset.annotate(new_recipe=Case(
|
||||||
When(created_at__gte=(timezone.now() - timedelta(days=7)), then=('pk')), default=Value(0), ))
|
When(created_at__gte=(timezone.now() - timedelta(days=7)), then=('pk')), default=Value(0), ))
|
||||||
@ -144,7 +152,7 @@ def search_recipes(request, queryset, params):
|
|||||||
queryset = queryset.filter(name__icontains=search_string)
|
queryset = queryset.filter(name__icontains=search_string)
|
||||||
|
|
||||||
if len(search_keywords) > 0:
|
if len(search_keywords) > 0:
|
||||||
if search_keywords_or == 'true':
|
if search_keywords_or:
|
||||||
# TODO creating setting to include descendants of keywords a setting
|
# TODO creating setting to include descendants of keywords a setting
|
||||||
# for kw in Keyword.objects.filter(pk__in=search_keywords):
|
# for kw in Keyword.objects.filter(pk__in=search_keywords):
|
||||||
# search_keywords += list(kw.get_descendants().values_list('pk', flat=True))
|
# search_keywords += list(kw.get_descendants().values_list('pk', flat=True))
|
||||||
@ -156,7 +164,7 @@ def search_recipes(request, queryset, params):
|
|||||||
queryset = queryset.filter(keywords__id__in=list(kw.get_descendants_and_self().values_list('pk', flat=True)))
|
queryset = queryset.filter(keywords__id__in=list(kw.get_descendants_and_self().values_list('pk', flat=True)))
|
||||||
|
|
||||||
if len(search_foods) > 0:
|
if len(search_foods) > 0:
|
||||||
if search_foods_or == 'true':
|
if search_foods_or:
|
||||||
# TODO creating setting to include descendants of food a setting
|
# TODO creating setting to include descendants of food a setting
|
||||||
queryset = queryset.filter(steps__ingredients__food__id__in=search_foods)
|
queryset = queryset.filter(steps__ingredients__food__id__in=search_foods)
|
||||||
else:
|
else:
|
||||||
@ -166,7 +174,7 @@ def search_recipes(request, queryset, params):
|
|||||||
queryset = queryset.filter(steps__ingredients__food__id__in=list(fd.get_descendants_and_self().values_list('pk', flat=True)))
|
queryset = queryset.filter(steps__ingredients__food__id__in=list(fd.get_descendants_and_self().values_list('pk', flat=True)))
|
||||||
|
|
||||||
if len(search_books) > 0:
|
if len(search_books) > 0:
|
||||||
if search_books_or == 'true':
|
if search_books_or:
|
||||||
queryset = queryset.filter(recipebookentry__book__id__in=search_books)
|
queryset = queryset.filter(recipebookentry__book__id__in=search_books)
|
||||||
else:
|
else:
|
||||||
for k in search_books:
|
for k in search_books:
|
||||||
@ -183,58 +191,119 @@ def search_recipes(request, queryset, params):
|
|||||||
if search_units:
|
if search_units:
|
||||||
queryset = queryset.filter(steps__ingredients__unit__id=search_units)
|
queryset = queryset.filter(steps__ingredients__unit__id=search_units)
|
||||||
|
|
||||||
if search_internal == 'true':
|
if search_internal:
|
||||||
queryset = queryset.filter(internal=True)
|
queryset = queryset.filter(internal=True)
|
||||||
|
|
||||||
queryset = queryset.distinct()
|
queryset = queryset.distinct()
|
||||||
|
|
||||||
if search_random == 'true':
|
if search_random:
|
||||||
queryset = queryset.order_by("?")
|
queryset = queryset.order_by("?")
|
||||||
else:
|
else:
|
||||||
# TODO add order by user settings
|
|
||||||
# orderby += ['name']
|
|
||||||
queryset = queryset.order_by(*orderby)
|
queryset = queryset.order_by(*orderby)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
def get_facet(qs, request):
|
def get_facet(qs=None, request=None, use_cache=True, hash_key=None):
|
||||||
# NOTE facet counts for tree models include self AND descendants
|
"""
|
||||||
|
Gets an annotated list from a queryset.
|
||||||
|
:param qs:
|
||||||
|
|
||||||
|
recipe queryset to build facets from
|
||||||
|
|
||||||
|
:param request:
|
||||||
|
|
||||||
|
the web request that contains the necessary query parameters
|
||||||
|
|
||||||
|
:param use_cache:
|
||||||
|
|
||||||
|
will find results in cache, if any, and return them or empty list.
|
||||||
|
will save the list of recipes IDs in the cache for future processing
|
||||||
|
|
||||||
|
:param hash_key:
|
||||||
|
|
||||||
|
the cache key of the recipe list to process
|
||||||
|
only evaluated if the use_cache parameter is false
|
||||||
|
"""
|
||||||
facets = {}
|
facets = {}
|
||||||
keyword_list = request.query_params.getlist('keywords', [])
|
recipe_list = []
|
||||||
food_list = request.query_params.getlist('foods', [])
|
cache_timeout = 600
|
||||||
book_list = request.query_params.getlist('book', [])
|
|
||||||
search_keywords_or = request.query_params.get('keywords_or', True)
|
if use_cache:
|
||||||
search_foods_or = request.query_params.get('foods_or', True)
|
qs_hash = hash(frozenset(qs.values_list('pk')))
|
||||||
search_books_or = request.query_params.get('books_or', True)
|
facets['cache_key'] = str(qs_hash)
|
||||||
|
SEARCH_CACHE_KEY = f"recipes_filter_{qs_hash}"
|
||||||
|
if c := caches['default'].get(SEARCH_CACHE_KEY, None):
|
||||||
|
facets['Keywords'] = c['Keywords'] or []
|
||||||
|
facets['Foods'] = c['Foods'] or []
|
||||||
|
facets['Books'] = c['Books'] or []
|
||||||
|
facets['Ratings'] = c['Ratings'] or []
|
||||||
|
facets['Recent'] = c['Recent'] or []
|
||||||
|
else:
|
||||||
|
facets['Keywords'] = []
|
||||||
|
facets['Foods'] = []
|
||||||
|
facets['Books'] = []
|
||||||
|
rating_qs = qs.annotate(rating=Round(Avg(Case(When(cooklog__created_by=request.user, then='cooklog__rating'), default=Value(0)))))
|
||||||
|
facets['Ratings'] = dict(Counter(r.rating for r in rating_qs))
|
||||||
|
facets['Recent'] = ViewLog.objects.filter(
|
||||||
|
created_by=request.user, space=request.space,
|
||||||
|
created_at__gte=timezone.now() - timedelta(days=14) # TODO make days of recent recipe a setting
|
||||||
|
).values_list('recipe__pk', flat=True)
|
||||||
|
|
||||||
|
cached_search = {
|
||||||
|
'recipe_list': list(qs.values_list('id', flat=True)),
|
||||||
|
'keyword_list': request.query_params.getlist('keywords', []),
|
||||||
|
'food_list': request.query_params.getlist('foods', []),
|
||||||
|
'book_list': request.query_params.getlist('book', []),
|
||||||
|
'search_keywords_or': str2bool(request.query_params.get('keywords_or', True)),
|
||||||
|
'search_foods_or': str2bool(request.query_params.get('foods_or', True)),
|
||||||
|
'search_books_or': str2bool(request.query_params.get('books_or', True)),
|
||||||
|
'space': request.space,
|
||||||
|
'Ratings': facets['Ratings'],
|
||||||
|
'Recent': facets['Recent'],
|
||||||
|
'Keywords': facets['Keywords'],
|
||||||
|
'Foods': facets['Foods'],
|
||||||
|
'Books': facets['Books']
|
||||||
|
}
|
||||||
|
caches['default'].set(SEARCH_CACHE_KEY, cached_search, cache_timeout)
|
||||||
|
return facets
|
||||||
|
|
||||||
|
SEARCH_CACHE_KEY = f'recipes_filter_{hash_key}'
|
||||||
|
if c := caches['default'].get(SEARCH_CACHE_KEY, None):
|
||||||
|
recipe_list = c['recipe_list']
|
||||||
|
keyword_list = c['keyword_list']
|
||||||
|
food_list = c['food_list']
|
||||||
|
book_list = c['book_list']
|
||||||
|
search_keywords_or = c['search_keywords_or']
|
||||||
|
search_foods_or = c['search_foods_or']
|
||||||
|
search_books_or = c['search_books_or']
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
# if using an OR search, will annotate all keywords, otherwise, just those that appear in results
|
# if using an OR search, will annotate all keywords, otherwise, just those that appear in results
|
||||||
if search_keywords_or:
|
if search_keywords_or:
|
||||||
keywords = Keyword.objects.filter(space=request.space).annotate(recipe_count=Count('recipe'))
|
keywords = Keyword.objects.filter(space=request.space).annotate(recipe_count=Count('recipe'))
|
||||||
else:
|
else:
|
||||||
keywords = Keyword.objects.filter(recipe__in=qs, space=request.space).annotate(recipe_count=Count('recipe'))
|
keywords = Keyword.objects.filter(recipe__in=recipe_list, space=request.space).annotate(recipe_count=Count('recipe'))
|
||||||
# custom django-tree function annotates a queryset to make building a tree easier.
|
# 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
|
# see https://django-treebeard.readthedocs.io/en/latest/api.html#treebeard.models.Node.get_annotated_list_qs for details
|
||||||
kw_a = annotated_qs(keywords, root=True, fill=True)
|
kw_a = annotated_qs(keywords, root=True, fill=True)
|
||||||
|
|
||||||
# if using an OR search, will annotate all keywords, otherwise, just those that appear in results
|
# # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
|
||||||
if search_foods_or:
|
if search_foods_or:
|
||||||
foods = Food.objects.filter(space=request.space).annotate(recipe_count=Count('ingredient'))
|
foods = Food.objects.filter(space=request.space).annotate(recipe_count=Count('ingredient'))
|
||||||
else:
|
else:
|
||||||
foods = Food.objects.filter(ingredient__step__recipe__in=list(qs.values_list('id', flat=True)), space=request.space).annotate(recipe_count=Count('ingredient'))
|
foods = Food.objects.filter(ingredient__step__recipe__in=recipe_list, space=request.space).annotate(recipe_count=Count('ingredient'))
|
||||||
food_a = annotated_qs(foods, root=True, fill=True)
|
food_a = annotated_qs(foods, root=True, fill=True)
|
||||||
|
|
||||||
rating_qs = qs.annotate(rating=Round(Avg(Case(When(cooklog__created_by=request.user, then='cooklog__rating'), default=Value(0)))))
|
|
||||||
|
|
||||||
# TODO add rating facet
|
# TODO add rating facet
|
||||||
facets['Ratings'] = dict(Counter(r.rating for r in rating_qs))
|
|
||||||
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
|
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
|
||||||
facets['Foods'] = fill_annotated_parents(food_a, food_list)
|
facets['Foods'] = fill_annotated_parents(food_a, food_list)
|
||||||
# TODO add book facet
|
# TODO add book facet
|
||||||
facets['Books'] = []
|
facets['Books'] = []
|
||||||
facets['Recent'] = ViewLog.objects.filter(
|
c['Keywords'] = facets['Keywords']
|
||||||
created_by=request.user, space=request.space,
|
c['Foods'] = facets['Foods']
|
||||||
created_at__gte=timezone.now() - timedelta(days=14) # TODO make days of recent recipe a setting
|
c['Books'] = facets['Books']
|
||||||
).values_list('recipe__pk', flat=True)
|
caches['default'].set(SEARCH_CACHE_KEY, c, cache_timeout)
|
||||||
return facets
|
return facets
|
||||||
|
|
||||||
|
|
||||||
|
@ -385,7 +385,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
if len(self.ingredient_set.all().exclude(step=None)) > 0:
|
if self.ingredient_set.all().exclude(step=None).count() > 0:
|
||||||
raise ProtectedError(self.name + _(" is part of a recipe step and cannot be deleted"), self.ingredient_set.all().exclude(step=None))
|
raise ProtectedError(self.name + _(" is part of a recipe step and cannot be deleted"), self.ingredient_set.all().exclude(step=None))
|
||||||
else:
|
else:
|
||||||
return super().delete()
|
return super().delete()
|
||||||
|
@ -216,9 +216,9 @@ 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 and obj.has_children():
|
if recipes.count() == 0 and obj.has_children():
|
||||||
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
|
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 recipes.count() != 0:
|
||||||
return random.choice(recipes).image.url
|
return random.choice(recipes).image.url
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
@ -249,7 +249,7 @@ class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
|||||||
def get_image(self, obj):
|
def get_image(self, obj):
|
||||||
recipes = Recipe.objects.filter(steps__ingredients__unit=obj, space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
|
recipes = Recipe.objects.filter(steps__ingredients__unit=obj, space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
|
||||||
|
|
||||||
if len(recipes) != 0:
|
if recipes.count() != 0:
|
||||||
return random.choice(recipes).image.url
|
return random.choice(recipes).image.url
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
@ -330,10 +330,10 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
|||||||
# 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 and obj.has_children():
|
if recipes.count() == 0 and obj.has_children():
|
||||||
recipes = Recipe.objects.filter(steps__ingredients__food__in=obj.get_descendants(), 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 recipes.count() != 0:
|
||||||
return random.choice(recipes).image.url
|
return random.choice(recipes).image.url
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -109,6 +109,7 @@ urlpatterns = [
|
|||||||
path('api/backup/', api.get_backup, name='api_backup'),
|
path('api/backup/', api.get_backup, name='api_backup'),
|
||||||
path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'),
|
path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'),
|
||||||
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
|
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
|
||||||
|
path('api/get_facets/', api.get_facets, name='api_get_facets'),
|
||||||
|
|
||||||
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), # TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
|
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), # TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
|
||||||
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
|
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
|
||||||
|
@ -499,7 +499,7 @@ class RecipePagination(PageNumberPagination):
|
|||||||
max_page_size = 100
|
max_page_size = 100
|
||||||
|
|
||||||
def paginate_queryset(self, queryset, request, view=None):
|
def paginate_queryset(self, queryset, request, view=None):
|
||||||
self.facets = get_facet(queryset, request)
|
self.facets = get_facet(qs=queryset, request=request)
|
||||||
return super().paginate_queryset(queryset, request, view)
|
return super().paginate_queryset(queryset, request, view)
|
||||||
|
|
||||||
def get_paginated_response(self, data):
|
def get_paginated_response(self, data):
|
||||||
@ -906,3 +906,15 @@ def ingredient_from_string(request):
|
|||||||
},
|
},
|
||||||
status=200
|
status=200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@group_required('user')
|
||||||
|
def get_facets(request):
|
||||||
|
key = request.GET['hash']
|
||||||
|
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
'facets': get_facet(request=request, use_cache=False, hash_key=key),
|
||||||
|
},
|
||||||
|
status=200
|
||||||
|
)
|
||||||
|
@ -1,7 +1,71 @@
|
|||||||
from os import getenv
|
from os import getenv
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.middleware import RemoteUserMiddleware
|
from django.contrib.auth.middleware import RemoteUserMiddleware
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
|
|
||||||
class CustomRemoteUser(RemoteUserMiddleware):
|
class CustomRemoteUser(RemoteUserMiddleware):
|
||||||
header = getenv('PROXY_HEADER', 'HTTP_REMOTE_USER')
|
header = getenv('PROXY_HEADER', 'HTTP_REMOTE_USER')
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Gist code by vstoykov, you can check his original gist at:
|
||||||
|
https://gist.github.com/vstoykov/1390853/5d2e8fac3ca2b2ada8c7de2fb70c021e50927375
|
||||||
|
Changes:
|
||||||
|
Ignoring static file requests and a certain useless admin request from triggering the logger.
|
||||||
|
Updated statements to make it Python 3 friendly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def terminal_width():
|
||||||
|
"""
|
||||||
|
Function to compute the terminal width.
|
||||||
|
"""
|
||||||
|
width = 0
|
||||||
|
try:
|
||||||
|
import struct, fcntl, termios
|
||||||
|
s = struct.pack('HHHH', 0, 0, 0, 0)
|
||||||
|
x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
|
||||||
|
width = struct.unpack('HHHH', x)[1]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if width <= 0:
|
||||||
|
try:
|
||||||
|
width = int(getenv['COLUMNS'])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if width <= 0:
|
||||||
|
width = 80
|
||||||
|
return width
|
||||||
|
|
||||||
|
|
||||||
|
def SqlPrintingMiddleware(get_response):
|
||||||
|
def middleware(request):
|
||||||
|
response = get_response(request)
|
||||||
|
if (
|
||||||
|
not settings.DEBUG
|
||||||
|
or len(connection.queries) == 0
|
||||||
|
or request.path_info.startswith(settings.MEDIA_URL)
|
||||||
|
or '/admin/jsi18n/' in request.path_info
|
||||||
|
):
|
||||||
|
return response
|
||||||
|
|
||||||
|
indentation = 2
|
||||||
|
print("\n\n%s\033[1;35m[SQL Queries for]\033[1;34m %s\033[0m\n" % (" " * indentation, request.path_info))
|
||||||
|
width = terminal_width()
|
||||||
|
total_time = 0.0
|
||||||
|
for query in connection.queries:
|
||||||
|
nice_sql = query['sql'].replace('"', '').replace(',', ', ')
|
||||||
|
sql = "\033[1;31m[%s]\033[0m %s" % (query['time'], nice_sql)
|
||||||
|
total_time = total_time + float(query['time'])
|
||||||
|
while len(sql) > width - indentation:
|
||||||
|
#print("%s%s" % (" " * indentation, sql[:width - indentation]))
|
||||||
|
sql = sql[width - indentation:]
|
||||||
|
#print("%s%s\n" % (" " * indentation, sql))
|
||||||
|
replace_tuple = (" " * indentation, str(total_time))
|
||||||
|
print("%s\033[1;32m[TOTAL TIME: %s seconds]\033[0m" % replace_tuple)
|
||||||
|
print("%s\033[1;32m[TOTAL QUERIES: %s]\033[0m" % (" " * indentation, len(connection.queries)))
|
||||||
|
return response
|
||||||
|
return middleware
|
||||||
|
@ -16,8 +16,8 @@ import re
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
# from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
# load_dotenv()
|
load_dotenv()
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
# Get vars from .env files
|
# Get vars from .env files
|
||||||
@ -389,3 +389,6 @@ EMAIL_USE_TLS = bool(int(os.getenv('EMAIL_USE_TLS', False)))
|
|||||||
EMAIL_USE_SSL = bool(int(os.getenv('EMAIL_USE_SSL', False)))
|
EMAIL_USE_SSL = bool(int(os.getenv('EMAIL_USE_SSL', False)))
|
||||||
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'webmaster@localhost')
|
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'webmaster@localhost')
|
||||||
ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv('ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix
|
ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv('ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix
|
||||||
|
|
||||||
|
if os.getenv('SQL_DEBUG', False):
|
||||||
|
MIDDLEWARE += ('recipes.middleware.SqlPrintingMiddleware',)
|
||||||
|
@ -223,12 +223,6 @@
|
|||||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"/>
|
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"/>
|
||||||
<b-input-group-append>
|
<b-input-group-append>
|
||||||
<b-input-group-text style="width:85px">
|
<b-input-group-text style="width:85px">
|
||||||
<!-- <b-form-checkbox v-model="settings.search_books_or" name="check-button"
|
|
||||||
@change="refreshData(false)"
|
|
||||||
class="shadow-none" tyle="width: 100%" switch>
|
|
||||||
<span class="text-uppercase" v-if="settings.search_books_or">{{ $t('or') }}</span>
|
|
||||||
<span class="text-uppercase" v-else>{{ $t('and') }}</span>
|
|
||||||
</b-form-checkbox> -->
|
|
||||||
</b-input-group-text>
|
</b-input-group-text>
|
||||||
</b-input-group-append>
|
</b-input-group-append>
|
||||||
</b-input-group>
|
</b-input-group>
|
||||||
@ -303,8 +297,7 @@ import VueCookies from 'vue-cookies'
|
|||||||
|
|
||||||
Vue.use(VueCookies)
|
Vue.use(VueCookies)
|
||||||
|
|
||||||
import {ResolveUrlMixin} from "@/utils/utils";
|
import {ApiMixin, ResolveUrlMixin} from "@/utils/utils";
|
||||||
import {ApiMixin} from "@/utils/utils";
|
|
||||||
|
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"; // is this deprecated?
|
import LoadingSpinner from "@/components/LoadingSpinner"; // is this deprecated?
|
||||||
|
|
||||||
@ -325,7 +318,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
// this.Models and this.Actions inherited from ApiMixin
|
// this.Models and this.Actions inherited from ApiMixin
|
||||||
recipes: [],
|
recipes: [],
|
||||||
facets: [],
|
facets: {},
|
||||||
meal_plans: [],
|
meal_plans: [],
|
||||||
last_viewed_recipes: [],
|
last_viewed_recipes: [],
|
||||||
|
|
||||||
@ -387,7 +380,6 @@ export default {
|
|||||||
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
|
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
|
||||||
this.settings = Object.assign({}, this.settings, this.$cookies.get(SETTINGS_COOKIE_NAME))
|
this.settings = Object.assign({}, this.settings, this.$cookies.get(SETTINGS_COOKIE_NAME))
|
||||||
}
|
}
|
||||||
|
|
||||||
let urlParams = new URLSearchParams(window.location.search);
|
let urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
if (urlParams.has('keyword')) {
|
if (urlParams.has('keyword')) {
|
||||||
@ -398,6 +390,18 @@ export default {
|
|||||||
this.facets.Keywords.push({'id':x, 'name': 'loading...'})
|
this.facets.Keywords.push({'id':x, 'name': 'loading...'})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.facets.Foods = []
|
||||||
|
for (let x of this.settings.search_foods) {
|
||||||
|
this.facets.Foods.push({'id':x, 'name': 'loading...'})
|
||||||
|
}
|
||||||
|
this.facets.Keywords = []
|
||||||
|
for (let x of this.settings.search_keywords) {
|
||||||
|
this.facets.Keywords.push({'id':x, 'name': 'loading...'})
|
||||||
|
}
|
||||||
|
this.facets.Books = []
|
||||||
|
for (let x of this.settings.search_books) {
|
||||||
|
this.facets.Books.push({'id':x, 'name': 'loading...'})
|
||||||
|
}
|
||||||
this.loadMealPlan()
|
this.loadMealPlan()
|
||||||
this.refreshData(false)
|
this.refreshData(false)
|
||||||
})
|
})
|
||||||
@ -457,6 +461,9 @@ export default {
|
|||||||
this.pagination_count = result.data.count
|
this.pagination_count = result.data.count
|
||||||
|
|
||||||
this.facets = result.data.facets
|
this.facets = result.data.facets
|
||||||
|
if(this.facets?.cache_key) {
|
||||||
|
this.getFacets(this.facets.cache_key)
|
||||||
|
}
|
||||||
this.recipes = this.removeDuplicates(result.data.results, recipe => recipe.id)
|
this.recipes = this.removeDuplicates(result.data.results, recipe => recipe.id)
|
||||||
if (!this.searchFiltered){
|
if (!this.searchFiltered){
|
||||||
// if meal plans are being shown - filter out any meal plan recipes from the recipe list
|
// if meal plans are being shown - filter out any meal plan recipes from the recipe list
|
||||||
@ -530,6 +537,11 @@ export default {
|
|||||||
return [undefined, undefined]
|
return [undefined, undefined]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getFacets: function(hash) {
|
||||||
|
this.genericGetAPI('api_get_facets', {hash: hash}).then((response) => {
|
||||||
|
this.facets = {...this.facets, ...response.data.facets}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,6 +203,9 @@ export const ApiMixin = {
|
|||||||
});
|
});
|
||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
return apiClient[func](...parameters)
|
return apiClient[func](...parameters)
|
||||||
|
},
|
||||||
|
genericGetAPI: function(url, options) {
|
||||||
|
return axios.get(this.resolveDjangoUrl(url), {'params':options, 'emulateJSON': true})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,83 +3,83 @@
|
|||||||
"assets": {
|
"assets": {
|
||||||
"../../templates/sw.js": {
|
"../../templates/sw.js": {
|
||||||
"name": "../../templates/sw.js",
|
"name": "../../templates/sw.js",
|
||||||
"path": "..\\..\\templates\\sw.js"
|
"path": "../../templates/sw.js"
|
||||||
},
|
},
|
||||||
"js/chunk-2d0da313.js": {
|
"js/chunk-2d0da313.js": {
|
||||||
"name": "js/chunk-2d0da313.js",
|
"name": "js/chunk-2d0da313.js",
|
||||||
"path": "js\\chunk-2d0da313.js"
|
"path": "js/chunk-2d0da313.js"
|
||||||
},
|
},
|
||||||
"css/chunk-vendors.css": {
|
"css/chunk-vendors.css": {
|
||||||
"name": "css/chunk-vendors.css",
|
"name": "css/chunk-vendors.css",
|
||||||
"path": "css\\chunk-vendors.css"
|
"path": "css/chunk-vendors.css"
|
||||||
},
|
},
|
||||||
"js/chunk-vendors.js": {
|
"js/chunk-vendors.js": {
|
||||||
"name": "js/chunk-vendors.js",
|
"name": "js/chunk-vendors.js",
|
||||||
"path": "js\\chunk-vendors.js"
|
"path": "js/chunk-vendors.js"
|
||||||
},
|
},
|
||||||
"css/cookbook_view.css": {
|
"css/cookbook_view.css": {
|
||||||
"name": "css/cookbook_view.css",
|
"name": "css/cookbook_view.css",
|
||||||
"path": "css\\cookbook_view.css"
|
"path": "css/cookbook_view.css"
|
||||||
},
|
},
|
||||||
"js/cookbook_view.js": {
|
"js/cookbook_view.js": {
|
||||||
"name": "js/cookbook_view.js",
|
"name": "js/cookbook_view.js",
|
||||||
"path": "js\\cookbook_view.js"
|
"path": "js/cookbook_view.js"
|
||||||
},
|
},
|
||||||
"css/edit_internal_recipe.css": {
|
"css/edit_internal_recipe.css": {
|
||||||
"name": "css/edit_internal_recipe.css",
|
"name": "css/edit_internal_recipe.css",
|
||||||
"path": "css\\edit_internal_recipe.css"
|
"path": "css/edit_internal_recipe.css"
|
||||||
},
|
},
|
||||||
"js/edit_internal_recipe.js": {
|
"js/edit_internal_recipe.js": {
|
||||||
"name": "js/edit_internal_recipe.js",
|
"name": "js/edit_internal_recipe.js",
|
||||||
"path": "js\\edit_internal_recipe.js"
|
"path": "js/edit_internal_recipe.js"
|
||||||
},
|
},
|
||||||
"js/import_response_view.js": {
|
"js/import_response_view.js": {
|
||||||
"name": "js/import_response_view.js",
|
"name": "js/import_response_view.js",
|
||||||
"path": "js\\import_response_view.js"
|
"path": "js/import_response_view.js"
|
||||||
},
|
},
|
||||||
"css/meal_plan_view.css": {
|
"css/meal_plan_view.css": {
|
||||||
"name": "css/meal_plan_view.css",
|
"name": "css/meal_plan_view.css",
|
||||||
"path": "css\\meal_plan_view.css"
|
"path": "css/meal_plan_view.css"
|
||||||
},
|
},
|
||||||
"js/meal_plan_view.js": {
|
"js/meal_plan_view.js": {
|
||||||
"name": "js/meal_plan_view.js",
|
"name": "js/meal_plan_view.js",
|
||||||
"path": "js\\meal_plan_view.js"
|
"path": "js/meal_plan_view.js"
|
||||||
},
|
},
|
||||||
"css/model_list_view.css": {
|
"css/model_list_view.css": {
|
||||||
"name": "css/model_list_view.css",
|
"name": "css/model_list_view.css",
|
||||||
"path": "css\\model_list_view.css"
|
"path": "css/model_list_view.css"
|
||||||
},
|
},
|
||||||
"js/model_list_view.js": {
|
"js/model_list_view.js": {
|
||||||
"name": "js/model_list_view.js",
|
"name": "js/model_list_view.js",
|
||||||
"path": "js\\model_list_view.js"
|
"path": "js/model_list_view.js"
|
||||||
},
|
},
|
||||||
"js/offline_view.js": {
|
"js/offline_view.js": {
|
||||||
"name": "js/offline_view.js",
|
"name": "js/offline_view.js",
|
||||||
"path": "js\\offline_view.js"
|
"path": "js/offline_view.js"
|
||||||
},
|
},
|
||||||
"css/recipe_search_view.css": {
|
"css/recipe_search_view.css": {
|
||||||
"name": "css/recipe_search_view.css",
|
"name": "css/recipe_search_view.css",
|
||||||
"path": "css\\recipe_search_view.css"
|
"path": "css/recipe_search_view.css"
|
||||||
},
|
},
|
||||||
"js/recipe_search_view.js": {
|
"js/recipe_search_view.js": {
|
||||||
"name": "js/recipe_search_view.js",
|
"name": "js/recipe_search_view.js",
|
||||||
"path": "js\\recipe_search_view.js"
|
"path": "js/recipe_search_view.js"
|
||||||
},
|
},
|
||||||
"css/recipe_view.css": {
|
"css/recipe_view.css": {
|
||||||
"name": "css/recipe_view.css",
|
"name": "css/recipe_view.css",
|
||||||
"path": "css\\recipe_view.css"
|
"path": "css/recipe_view.css"
|
||||||
},
|
},
|
||||||
"js/recipe_view.js": {
|
"js/recipe_view.js": {
|
||||||
"name": "js/recipe_view.js",
|
"name": "js/recipe_view.js",
|
||||||
"path": "js\\recipe_view.js"
|
"path": "js/recipe_view.js"
|
||||||
},
|
},
|
||||||
"js/supermarket_view.js": {
|
"js/supermarket_view.js": {
|
||||||
"name": "js/supermarket_view.js",
|
"name": "js/supermarket_view.js",
|
||||||
"path": "js\\supermarket_view.js"
|
"path": "js/supermarket_view.js"
|
||||||
},
|
},
|
||||||
"js/user_file_view.js": {
|
"js/user_file_view.js": {
|
||||||
"name": "js/user_file_view.js",
|
"name": "js/user_file_view.js",
|
||||||
"path": "js\\user_file_view.js"
|
"path": "js/user_file_view.js"
|
||||||
},
|
},
|
||||||
"recipe_search_view.html": {
|
"recipe_search_view.html": {
|
||||||
"name": "recipe_search_view.html",
|
"name": "recipe_search_view.html",
|
||||||
|
Loading…
Reference in New Issue
Block a user