fuzzy match on lookups
This commit is contained in:
parent
32c488f4a8
commit
b556bed56e
@ -479,10 +479,11 @@ class SearchPreferenceForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SearchPreference
|
model = SearchPreference
|
||||||
fields = ('search', 'unaccent', 'icontains', 'istartswith', 'trigram', 'fulltext')
|
fields = ('search', 'lookup', 'unaccent', 'icontains', 'istartswith', 'trigram', 'fulltext')
|
||||||
|
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'search': _('Select type method of search. Click <a href="/docs/search/">here</a> for full desciption of choices.'),
|
'search': _('Select type method of search. Click <a href="/docs/search/">here</a> for full desciption of choices.'),
|
||||||
|
'lookup': _('Use fuzzy matching on units, keywords and ingredients when editing and importing recipes.'),
|
||||||
'unaccent': _('Fields to search ignoring accents. Selecting this option can improve or degrade search quality depending on language'),
|
'unaccent': _('Fields to search ignoring accents. Selecting this option can improve or degrade search quality depending on language'),
|
||||||
'icontains': _("Fields to search for partial matches. (e.g. searching for 'Pie' will return 'pie' and 'piece' and 'soapie')"),
|
'icontains': _("Fields to search for partial matches. (e.g. searching for 'Pie' will return 'pie' and 'piece' and 'soapie')"),
|
||||||
'istartswith': _("Fields to search for beginning of word matches. (e.g. searching for 'sa' will return 'salad' and 'sandwich')"),
|
'istartswith': _("Fields to search for beginning of word matches. (e.g. searching for 'sa' will return 'salad' and 'sandwich')"),
|
||||||
@ -492,6 +493,7 @@ class SearchPreferenceForm(forms.ModelForm):
|
|||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'search': _('Search Method'),
|
'search': _('Search Method'),
|
||||||
|
'lookup': _('Fuzzy Lookups'),
|
||||||
'unaccent': _('Ignore Accent'),
|
'unaccent': _('Ignore Accent'),
|
||||||
'icontains': _("Partial Match"),
|
'icontains': _("Partial Match"),
|
||||||
'istartswith': _("Starts Wtih"),
|
'istartswith': _("Starts Wtih"),
|
||||||
@ -506,4 +508,4 @@ class SearchPreferenceForm(forms.ModelForm):
|
|||||||
'istartswith': MultiSelectWidget,
|
'istartswith': MultiSelectWidget,
|
||||||
'trigram': MultiSelectWidget,
|
'trigram': MultiSelectWidget,
|
||||||
'fulltext': MultiSelectWidget,
|
'fulltext': MultiSelectWidget,
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,8 @@ class Migration(migrations.Migration):
|
|||||||
name='SearchPreference',
|
name='SearchPreference',
|
||||||
fields=[
|
fields=[
|
||||||
('user', annoying.fields.AutoOneToOneField(on_delete=deletion.CASCADE, primary_key=True, serialize=False, to='auth.user')),
|
('user', annoying.fields.AutoOneToOneField(on_delete=deletion.CASCADE, primary_key=True, serialize=False, to='auth.user')),
|
||||||
('search', models.CharField(choices=[('plain', 'Plain'), ('phrase', 'Phrase'), ('websearch', 'Web'), ('raw', 'Raw')], default='plain', max_length=32)),
|
('search', models.CharField(choices=[('plain', 'Simple'), ('phrase', 'Phrase'), ('websearch', 'Web'), ('raw', 'Raw')], default='plain', max_length=32)),
|
||||||
|
('lookup', models.BooleanField(default=False)),
|
||||||
('fulltext', models.ManyToManyField(blank=True, related_name='fulltext_fields', to='cookbook.SearchFields')),
|
('fulltext', models.ManyToManyField(blank=True, related_name='fulltext_fields', to='cookbook.SearchFields')),
|
||||||
('icontains', models.ManyToManyField(blank=True, default=nameSearchField, related_name='icontains_fields', to='cookbook.SearchFields')),
|
('icontains', models.ManyToManyField(blank=True, default=nameSearchField, related_name='icontains_fields', to='cookbook.SearchFields')),
|
||||||
('istartswith', models.ManyToManyField(blank=True, related_name='istartswith_fields', to='cookbook.SearchFields')),
|
('istartswith', models.ManyToManyField(blank=True, related_name='istartswith_fields', to='cookbook.SearchFields')),
|
||||||
|
@ -760,6 +760,7 @@ class SearchPreference(models.Model, PermissionModelMixin):
|
|||||||
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||||
search = models.CharField(choices=SEARCH_STYLE, max_length=32, default=SIMPLE)
|
search = models.CharField(choices=SEARCH_STYLE, max_length=32, default=SIMPLE)
|
||||||
|
|
||||||
|
lookup = models.BooleanField(default=False)
|
||||||
unaccent = models.ManyToManyField(SearchFields, related_name="unaccent_fields", blank=True, default=allSearchFields)
|
unaccent = models.ManyToManyField(SearchFields, related_name="unaccent_fields", blank=True, default=allSearchFields)
|
||||||
icontains = models.ManyToManyField(SearchFields, related_name="icontains_fields", blank=True, default=nameSearchField)
|
icontains = models.ManyToManyField(SearchFields, related_name="icontains_fields", blank=True, default=nameSearchField)
|
||||||
istartswith = models.ManyToManyField(SearchFields, related_name="istartswith_fields", blank=True)
|
istartswith = models.ManyToManyField(SearchFields, related_name="istartswith_fields", blank=True)
|
||||||
|
File diff suppressed because one or more lines are too long
@ -171,5 +171,15 @@
|
|||||||
token.select();
|
token.select();
|
||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
}
|
}
|
||||||
|
// Javascript to enable link to tab
|
||||||
|
var hash = location.hash.replace(/^#/, ''); // ^ means starting, meaning only match the first hash
|
||||||
|
if (hash) {
|
||||||
|
$('.nav-tabs a[href="#' + hash + '"]').tab('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change hash for page-reload
|
||||||
|
$('.nav-tabs a').on('shown.bs.tab', function (e) {
|
||||||
|
window.location.hash = e.target.hash;
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -9,6 +9,7 @@ from annoying.decorators import ajax_request
|
|||||||
from annoying.functions import get_object_or_None
|
from annoying.functions import get_object_or_None
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.postgres.search import TrigramSimilarity
|
||||||
from django.core.exceptions import FieldError, ValidationError
|
from django.core.exceptions import FieldError, ValidationError
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@ -88,6 +89,38 @@ class StandardFilterMixin(ViewSetMixin):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class FuzzyFilterMixin(ViewSetMixin):
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = self.queryset
|
||||||
|
query = self.request.query_params.get('query', None)
|
||||||
|
fuzzy = self.request.user.searchpreference.lookup
|
||||||
|
|
||||||
|
if query is not None or query != '':
|
||||||
|
if fuzzy:
|
||||||
|
queryset = queryset.annotate(trigram=TrigramSimilarity('name', query)).filter(trigram__gt=0.2).order_by("-trigram")
|
||||||
|
else:
|
||||||
|
# TODO have this check unaccent search settings?
|
||||||
|
queryset = queryset.filter(name__icontains=query)
|
||||||
|
|
||||||
|
updated_at = self.request.query_params.get('updated_at', None)
|
||||||
|
if updated_at is not None:
|
||||||
|
try:
|
||||||
|
queryset = queryset.filter(updated_at__gte=updated_at)
|
||||||
|
except FieldError:
|
||||||
|
pass
|
||||||
|
except ValidationError:
|
||||||
|
raise APIException(_('Parameter updated_at incorrectly formatted'))
|
||||||
|
|
||||||
|
limit = self.request.query_params.get('limit', None)
|
||||||
|
random = self.request.query_params.get('random', False)
|
||||||
|
if limit is not None:
|
||||||
|
if random:
|
||||||
|
queryset = queryset.order_by("?")
|
||||||
|
queryset = queryset[:int(limit)]
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class UserNameViewSet(viewsets.ReadOnlyModelViewSet):
|
class UserNameViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
list:
|
list:
|
||||||
@ -159,27 +192,7 @@ class SupermarketViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
|||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
|
|
||||||
|
|
||||||
class SupermarketCategoryViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
class KeywordViewSet(viewsets.ModelViewSet, FuzzyFilterMixin):
|
||||||
queryset = SupermarketCategory.objects
|
|
||||||
serializer_class = SupermarketCategorySerializer
|
|
||||||
permission_classes = [CustomIsUser]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
self.queryset = self.queryset.filter(space=self.request.space)
|
|
||||||
return super().get_queryset()
|
|
||||||
|
|
||||||
|
|
||||||
class SupermarketCategoryRelationViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
|
||||||
queryset = SupermarketCategoryRelation.objects
|
|
||||||
serializer_class = SupermarketCategoryRelationSerializer
|
|
||||||
permission_classes = [CustomIsUser]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
self.queryset = self.queryset.filter(supermarket__space=self.request.space)
|
|
||||||
return super().get_queryset()
|
|
||||||
|
|
||||||
|
|
||||||
class KeywordViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
|
||||||
"""
|
"""
|
||||||
list:
|
list:
|
||||||
optional parameters
|
optional parameters
|
||||||
@ -197,7 +210,7 @@ class KeywordViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
|||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
|
|
||||||
|
|
||||||
class UnitViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
class UnitViewSet(viewsets.ModelViewSet, FuzzyFilterMixin):
|
||||||
queryset = Unit.objects
|
queryset = Unit.objects
|
||||||
serializer_class = UnitSerializer
|
serializer_class = UnitSerializer
|
||||||
permission_classes = [CustomIsUser]
|
permission_classes = [CustomIsUser]
|
||||||
@ -207,7 +220,7 @@ class UnitViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
|||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
|
|
||||||
|
|
||||||
class FoodViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
class FoodViewSet(viewsets.ModelViewSet, FuzzyFilterMixin):
|
||||||
queryset = Food.objects
|
queryset = Food.objects
|
||||||
serializer_class = FoodSerializer
|
serializer_class = FoodSerializer
|
||||||
permission_classes = [CustomIsUser]
|
permission_classes = [CustomIsUser]
|
||||||
|
@ -34,7 +34,6 @@ from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
|
|||||||
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall,
|
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall,
|
||||||
ViewLogTable, InviteLinkTable)
|
ViewLogTable, InviteLinkTable)
|
||||||
from cookbook.views.data import Object
|
from cookbook.views.data import Object
|
||||||
from recipes import settings
|
|
||||||
from recipes.settings import DEMO
|
from recipes.settings import DEMO
|
||||||
from recipes.version import BUILD_REF, VERSION_NUMBER
|
from recipes.version import BUILD_REF, VERSION_NUMBER
|
||||||
|
|
||||||
@ -374,6 +373,7 @@ def user_settings(request):
|
|||||||
search_error = True
|
search_error = True
|
||||||
else:
|
else:
|
||||||
sp.search = search_form.cleaned_data['search']
|
sp.search = search_form.cleaned_data['search']
|
||||||
|
sp.lookup = search_form.cleaned_data['lookup']
|
||||||
sp.unaccent.set(search_form.cleaned_data['unaccent'])
|
sp.unaccent.set(search_form.cleaned_data['unaccent'])
|
||||||
sp.icontains.set(search_form.cleaned_data['icontains'])
|
sp.icontains.set(search_form.cleaned_data['icontains'])
|
||||||
sp.istartswith.set(search_form.cleaned_data['istartswith'])
|
sp.istartswith.set(search_form.cleaned_data['istartswith'])
|
||||||
@ -398,6 +398,7 @@ def user_settings(request):
|
|||||||
# these fields require postgress - just disable them if postgress isn't available
|
# these fields require postgress - just disable them if postgress isn't available
|
||||||
if not settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
|
if not settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
|
||||||
search_form.fields['search'].disabled = True
|
search_form.fields['search'].disabled = True
|
||||||
|
search_form.fields['lookup'].disabled = True
|
||||||
search_form.fields['trigram'].disabled = True
|
search_form.fields['trigram'].disabled = True
|
||||||
search_form.fields['fulltext'].disabled = True
|
search_form.fields['fulltext'].disabled = True
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row" style="margin-top: 1vh">
|
<div class="row" style="margin-top: 1vh">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<a :href="resolveDjangoUrl('view_settings')">{{ $t('Advanced Search Settings') }}</a>
|
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{ $t('Advanced Search Settings') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="margin-top: 1vh">
|
<div class="row" style="margin-top: 1vh">
|
||||||
|
Loading…
Reference in New Issue
Block a user