fixes keyword filter on OR search

This commit is contained in:
smilerz 2021-08-24 22:01:02 -05:00
parent 7bab07bdaf
commit 2808e3033d
22 changed files with 41 additions and 59 deletions

View File

@ -1,5 +1,7 @@
import django_filters import django_filters
from django.conf import settings 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.utils.translation import gettext as _
from django_scopes import scopes_disabled from django_scopes import scopes_disabled

View File

@ -174,22 +174,22 @@ def search_recipes(request, queryset, params):
return queryset return queryset
def get_facet(qs, request): def get_facet(qs, params, space):
# NOTE facet counts for tree models include self AND descendants # NOTE facet counts for tree models include self AND descendants
facets = {} facets = {}
ratings = request.query_params.getlist('ratings', []) ratings = params.getlist('ratings', [])
keyword_list = request.query_params.getlist('keywords', []) keyword_list = params.getlist('keywords', [])
food_list = request.query_params.getlist('foods', []) ingredient_list = params.getlist('foods', [])
book_list = request.query_params.getlist('book', []) book_list = params.getlist('book', [])
search_keywords_or = request.query_params.get('keywords_or', True) search_keywords_or = params.get('keywords_or', True)
search_foods_or = request.query_params.get('foods_or', True) search_foods_or = params.get('foods_or', True)
search_books_or = request.query_params.get('books_or', True) search_books_or = params.get('books_or', 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_keywords_or: if search_keywords_or:
keywords = Keyword.objects.filter(space=request.space).annotate(recipe_count=Count('recipe')) keywords = Keyword.objects.filter(space=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=qs, space=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)

View File

@ -23,7 +23,7 @@ class Mealie(Integration):
name=recipe_json['name'].strip(), description=description, name=recipe_json['name'].strip(), description=description,
created_by=self.request.user, internal=True, space=self.request.space) created_by=self.request.user, internal=True, space=self.request.space)
# TODO parse times (given in PT2H3M ) # TODO parse times (given in PT2H3M )
# @vabene check recipe_url_import.iso_duration_to_minutes I think it does what you are looking for # @vabene check recipe_url_import.iso_duration_to_minutes I think it does what you are looking for
ingredients_added = False ingredients_added = False

View File

@ -37,16 +37,14 @@ def get_model_name(model):
class TreeManager(MP_NodeManager): class TreeManager(MP_NodeManager):
# model.Manager get_or_create() is not compatible with MP_Tree
def get_or_create(self, **kwargs): def get_or_create(self, **kwargs):
# model.Manager get_or_create() is not compatible with MP_Tree
kwargs['name'] = kwargs['name'].strip() kwargs['name'] = kwargs['name'].strip()
q = self.filter(name__iexact=kwargs['name'], space=kwargs['space']) try:
if len(q) != 0: return self.get(name__iexact=kwargs['name'], space=kwargs['space']), False
return q[0], False except self.model.DoesNotExist:
else:
with scopes_disabled(): with scopes_disabled():
node = self.model.add_root(**kwargs) return self.model.add_root(**kwargs), True
return node, True
class PermissionModelMixin: class PermissionModelMixin:

View File

@ -63,7 +63,6 @@ class RecipeSchema(AutoSchema):
class TreeSchema(AutoSchema): class TreeSchema(AutoSchema):
def get_path_parameters(self, path, method): def get_path_parameters(self, path, method):
if not is_list_view(path, method, self.view): if not is_list_view(path, method, self.view):
return super(TreeSchema, self).get_path_parameters(path, method) return super(TreeSchema, self).get_path_parameters(path, method)
@ -85,5 +84,4 @@ class TreeSchema(AutoSchema):
"description": 'Return all self and children of {} with ID [int].'.format(api_name), "description": 'Return all self and children of {} with ID [int].'.format(api_name),
'schema': {'type': 'int', }, 'schema': {'type': 'int', },
}) })
return parameters return parameters

View File

@ -3,8 +3,7 @@ from datetime import timedelta
from decimal import Decimal from decimal import Decimal
from gettext import gettext as _ from gettext import gettext as _
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import QuerySet, Sum, Avg from django.db.models import Avg, QuerySet, Sum
from django.utils import timezone
from drf_writable_nested import (UniqueFieldsMixin, from drf_writable_nested import (UniqueFieldsMixin,
WritableNestedModelSerializer) WritableNestedModelSerializer)
from rest_framework import serializers from rest_framework import serializers
@ -213,9 +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( 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
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:
@ -233,7 +231,6 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
return obj return obj
class Meta: class Meta:
# list_serializer_class = SpaceFilterSerializer
model = Keyword model = Keyword
fields = ( fields = (
'id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at', 'id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',

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

@ -22,7 +22,6 @@ MOVE_URL = 'api:keyword-move'
MERGE_URL = 'api:keyword-merge' MERGE_URL = 'api:keyword-merge'
# TODO are there better ways to manage these fixtures?
@pytest.fixture() @pytest.fixture()
def obj_1(space_1): def obj_1(space_1):
return Keyword.objects.get_or_create(name='test_1', space=space_1)[0] return Keyword.objects.get_or_create(name='test_1', space=space_1)[0]
@ -352,7 +351,6 @@ def test_merge(
def test_root_filter(obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, u1_s1): def test_root_filter(obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, u1_s1):
# should return root objects in the space (obj_1, obj_2), ignoring query filters # should return root objects in the space (obj_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root=0').content) response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root=0').content)
assert len(response['results']) == 2 assert len(response['results']) == 2
@ -367,7 +365,6 @@ def test_root_filter(obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, u1_s1):
def test_tree_filter(obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, u1_s1): def test_tree_filter(obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, u1_s1):
with scopes_disabled(): with scopes_disabled():
obj_2.move(obj_1, 'sorted-child') obj_2.move(obj_1, 'sorted-child')
# should return full tree starting at obj_1 (obj_1_1_1, obj_2), ignoring query filters # should return full tree starting at obj_1 (obj_1_1_1, obj_2), ignoring query filters

View File

@ -109,9 +109,9 @@ urlpatterns = [
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('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), # TODO is this deprecated?
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'), path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'), # TODO is this deprecated?
path('telegram/setup/<int:pk>', telegram.setup_bot, name='telegram_setup'), path('telegram/setup/<int:pk>', telegram.setup_bot, name='telegram_setup'),
path('telegram/remove/<int:pk>', telegram.remove_bot, name='telegram_remove'), path('telegram/remove/<int:pk>', telegram.remove_bot, name='telegram_remove'),
@ -137,7 +137,7 @@ urlpatterns = [
generic_models = ( generic_models = (
Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync, Recipe, RecipeImport, Storage, RecipeBook, MealPlan, SyncLog, Sync,
Comment, RecipeBookEntry, Keyword, Food, ShoppingList, InviteLink Comment, RecipeBookEntry, Food, ShoppingList, InviteLink
) )
for m in generic_models: for m in generic_models:

View File

@ -113,11 +113,8 @@ class FuzzyFilterMixin(ViewSetMixin):
) )
else: else:
# TODO have this check unaccent search settings or other search preferences? # TODO have this check unaccent search settings or other search preferences?
self.queryset = ( # TODO for some querysets exact matches are sorted beyond pagesize, need to find better solution
self.queryset self.queryset = self.queryset.filter(name__istartswith=query) | self.queryset.filter(name__icontains=query)
.annotate(exact=Case(When(name__iexact=query, then=(Value(100))), default=Value(0))) # put exact matches at the top of the result set
.filter(name__icontains=query).order_by('-exact')
)
updated_at = self.request.query_params.get('updated_at', None) updated_at = self.request.query_params.get('updated_at', None)
if updated_at is not None: if updated_at is not None:
@ -152,14 +149,14 @@ class TreeMixin(FuzzyFilterMixin):
except self.model.DoesNotExist: except self.model.DoesNotExist:
self.queryset = self.model.objects.none() self.queryset = self.model.objects.none()
if root == 0: if root == 0:
self.queryset = self.model.get_root_nodes() | self.model.objects.filter(depth=0) self.queryset = self.model.get_root_nodes()
else: else:
self.queryset = self.model.objects.get(id=root).get_children() self.queryset = self.model.objects.get(id=root).get_children()
elif tree: elif tree:
if tree.isnumeric(): if tree.isnumeric():
try: try:
self.queryset = self.model.objects.get(id=int(tree)).get_descendants_and_self() self.queryset = self.model.objects.get(id=int(tree)).get_descendants_and_self()
except Keyword.DoesNotExist: except self.model.DoesNotExist:
self.queryset = self.model.objects.none() self.queryset = self.model.objects.none()
else: else:
return super().get_queryset() return super().get_queryset()
@ -470,7 +467,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(queryset, request.query_params, request.space)
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):

View File

@ -148,7 +148,6 @@ def import_url(request):
recipe.steps.add(step) recipe.steps.add(step)
all_keywords = Keyword.get_tree()
for kw in data['keywords']: for kw in data['keywords']:
k, created = Keyword.objects.get_or_create(name=kw['text'], space=request.space) k, created = Keyword.objects.get_or_create(name=kw['text'], space=request.space)
recipe.keywords.add(k) recipe.keywords.add(k)

View File

@ -56,9 +56,7 @@ def index(request):
return HttpResponseRedirect(reverse('view_search')) return HttpResponseRedirect(reverse('view_search'))
# faceting # TODO need to deprecate
# unaccent / likely will perform full table scan
# create tests
def search(request): def search(request):
if has_group_permission(request.user, ('guest',)): if has_group_permission(request.user, ('guest',)):
if request.user.userpreference.search_style == UserPreference.NEW: if request.user.userpreference.search_style == UserPreference.NEW:

View File

@ -8,7 +8,6 @@
<div class="col-xl-8 col-12"> <div class="col-xl-8 col-12">
<!-- TODO only show scollbars in split mode, but this doesn't interact well with infinite scroll, maybe a different component? --> <!-- TODO only show scollbars in split mode, but this doesn't interact well with infinite scroll, maybe a different component? -->
<div class="container-fluid d-flex flex-column flex-grow-1" :class="{'vh-100' : show_split}"> <div class="container-fluid d-flex flex-column flex-grow-1" :class="{'vh-100' : show_split}">
<!-- <div class="container-fluid d-flex flex-column flex-grow-1 vh-100"> -->
<!-- expanded options box --> <!-- expanded options box -->
<div class="row flex-shrink-0"> <div class="row flex-shrink-0">
<div class="col col-md-12"> <div class="col col-md-12">

View File

@ -27,7 +27,7 @@
<script> <script>
export default { export default {
name: 'KeywordContextMenu', name: 'GenericContextMenu',
props: { props: {
show_edit: {type: Boolean, default: true}, show_edit: {type: Boolean, default: true},
show_delete: {type: Boolean, default: true}, show_delete: {type: Boolean, default: true},

View File

@ -36,10 +36,7 @@ export default {
search_function: String, search_function: String,
label: String, label: String,
parent_variable: {type: String, default: undefined}, parent_variable: {type: String, default: undefined},
limit: { limit: {type: Number, default: 10,},
type: Number,
default: 10,
},
sticky_options: {type:Array, default(){return []}}, sticky_options: {type:Array, default(){return []}},
initial_selection: {type:Array, default(){return []}}, initial_selection: {type:Array, default(){return []}},
multiple: {type: Boolean, default: true}, multiple: {type: Boolean, default: true},

View File

@ -80,7 +80,7 @@ module.exports = {
}, },
}, },
}, },
// TODO make this conditional on .env DEBUG = TRUE // TODO make this conditional on .env DEBUG = FALSE
config.optimization.minimize(true) config.optimization.minimize(true)
); );