From 591591e3dc936de37ee80e3cbb0ec64cef29605c Mon Sep 17 00:00:00 2001 From: smilerz Date: Fri, 4 Jun 2021 13:33:02 -0500 Subject: [PATCH] search preference settings --- cookbook/forms.py | 38 +- ...index.py => 0124_build_full_text_index.py} | 34 +- .../0125_create_searchfields.py.stop | 23 + cookbook/models.py | 50 +- cookbook/templates/settings.html | 14 + cookbook/views/views.py | 31 +- vue/webpack-stats.json | 2 +- vue/yarn.lock | 1148 ++++++++--------- 8 files changed, 723 insertions(+), 617 deletions(-) rename cookbook/migrations/{0122_build_full_text_index.py => 0124_build_full_text_index.py} (62%) create mode 100644 cookbook/migrations/0125_create_searchfields.py.stop diff --git a/cookbook/forms.py b/cookbook/forms.py index 801ff5fb..2cbefd9e 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -10,7 +10,8 @@ from hcaptcha.fields import hCaptchaField from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe, RecipeBook, RecipeBookEntry, Storage, Sync, Unit, User, - UserPreference, SupermarketCategory, MealType, Space) + UserPreference, SupermarketCategory, MealType, Space, + SearchPreference) class SelectWidget(widgets.Select): @@ -476,3 +477,38 @@ class UserCreateForm(forms.Form): attrs={'autocomplete': 'new-password', 'type': 'password'} ) ) + + +class SearchPreferenceForm(forms.ModelForm): + prefix = 'search' + + class Meta: + model = SearchPreference + fields = ('search', 'unaccent', 'icontains', 'istartswith', 'trigram', 'fulltext') + + help_texts = { + 'search': _('Select type method of search. Click here for full desciption of choices.'), + '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')"), + 'istartswith': _("Fields to search for beginning of word matches. (e.g. searching for 'sa' will return 'salad' and 'sandwich')"), + 'trigram': _("Fields to 'fuzzy' search. (e.g. searching for 'recpie' will find 'recipe'.) Note: this option will conflict with 'web' and 'raw' methods of search."), + 'fulltext': _("Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods only function with fulltext fields.") + } + + labels = { + 'search': _('Search Method'), + 'unaccent': _('Ignore Accent'), + 'icontains': _("Partial Match"), + 'istartswith': _("Starts Wtih"), + 'trigram': _("Fuzzy Search"), + 'fulltext': _("Full Text") + } + + widgets = { + 'search': SelectWidget, + 'unaccent': MultiSelectWidget, + 'icontains': MultiSelectWidget, + 'istartswith': MultiSelectWidget, + 'trigram': MultiSelectWidget, + 'fulltext': MultiSelectWidget, + } diff --git a/cookbook/migrations/0122_build_full_text_index.py b/cookbook/migrations/0124_build_full_text_index.py similarity index 62% rename from cookbook/migrations/0122_build_full_text_index.py rename to cookbook/migrations/0124_build_full_text_index.py index 7741e2f1..5a6776c1 100644 --- a/cookbook/migrations/0122_build_full_text_index.py +++ b/cookbook/migrations/0124_build_full_text_index.py @@ -1,14 +1,14 @@ # Generated by Django 3.1.7 on 2021-04-07 20:00 +import annoying.fields from django.conf import settings from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.search import SearchVectorField, SearchVector -from django.db import migrations +from django.db import migrations, models +from django.db.models import deletion from django_scopes import scopes_disabled from django.utils import translation from cookbook.managers import DICTIONARY -from cookbook.models import Recipe, Step, Index - - +from cookbook.models import Recipe, Step, Index, PermissionModelMixin, nameSearchField, allSearchFields def set_default_search_vector(apps, schema_editor): @@ -17,6 +17,7 @@ def set_default_search_vector(apps, schema_editor): language = DICTIONARY.get(translation.get_language(), 'simple') with scopes_disabled(): # TODO this approach doesn't work terribly well if multiple languages are in use + # I'm also uncertain about forcing unaccent here Recipe.objects.all().update( name_search_vector=SearchVector('name__unaccent', weight='A', config=language), desc_search_vector=SearchVector('description__unaccent', weight='B', config=language) @@ -26,7 +27,8 @@ def set_default_search_vector(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('cookbook', '0121_auto_20210518_1638'), + ('auth', '0012_alter_user_first_name_max_length'), + ('cookbook', '0123_invitelink_email'), ] operations = [ migrations.AddField( @@ -80,6 +82,28 @@ class Migration(migrations.Migration): model_name='viewlog', index=Index(fields=['recipe', '-created_at'], name='cookbook_vi_recipe__5cd178_idx'), ), + migrations.CreateModel( + name='SearchFields', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32, unique=True)), + ('field', models.CharField(max_length=64, unique=True)), + ], + bases=(models.Model, PermissionModelMixin), + ), + migrations.CreateModel( + name='SearchPreference', + fields=[ + ('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='SIMPLE', max_length=32)), + ('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')), + ('istartswith', models.ManyToManyField(blank=True, related_name='istartswith_fields', to='cookbook.SearchFields')), + ('trigram', models.ManyToManyField(blank=True, related_name='trigram_fields', to='cookbook.SearchFields')), + ('unaccent', models.ManyToManyField(blank=True, default=allSearchFields, related_name='unaccent_fields', to='cookbook.SearchFields')), + ], + bases=(models.Model, PermissionModelMixin), + ), migrations.RunPython( set_default_search_vector ), diff --git a/cookbook/migrations/0125_create_searchfields.py.stop b/cookbook/migrations/0125_create_searchfields.py.stop new file mode 100644 index 00000000..991342a9 --- /dev/null +++ b/cookbook/migrations/0125_create_searchfields.py.stop @@ -0,0 +1,23 @@ +from cookbook.models import SearchFields +from django.db import migrations + + +def create_searchfields(apps, schema_editor): + SearchFields.objects.create(name='Name', field='name') + SearchFields.objects.create(name='Description', field='description') + SearchFields.objects.create(name='Instructions', field='steps__instruction') + SearchFields.objects.create(name='Ingredients', field='steps__ingredients__food__name') + SearchFields.objects.create(name='Keywords', field='keywords__name') + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0124_build_full_text_index'), + ] + + operations = [ + migrations.RunPython( + create_searchfields + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index 06c34a8b..64c09a35 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -105,7 +105,8 @@ class UserPreference(models.Model, PermissionModelMixin): COLORS = ( (PRIMARY, 'Primary'), (SECONDARY, 'Secondary'), - (SUCCESS, 'Success'), (INFO, 'Info'), + (SUCCESS, 'Success'), + (INFO, 'Info'), (WARNING, 'Warning'), (DANGER, 'Danger'), (LIGHT, 'Light'), @@ -721,3 +722,50 @@ class BookmarkletImport(ExportModelOperationsMixin('bookmarklet_import'), models objects = ScopedManager(space='space') space = models.ForeignKey(Space, on_delete=models.CASCADE) + + +# field names used to configure search behavior - all data populated during data migration +# other option is to use a MultiSelectField from https://github.com/goinnn/django-multiselectfield +class SearchFields(models.Model, PermissionModelMixin): + name = models.CharField(max_length=32, unique=True) + field = models.CharField(max_length=64, unique=True) + + def __str__(self): + return _(self.name) + + @staticmethod + def get_name(self): + return _(self.name) + + +def allSearchFields(): + return SearchFields.objects.values_list('id') + + +def nameSearchField(): + return [SearchFields.objects.get(name='Name').id] + + +class SearchPreference(models.Model, PermissionModelMixin): + # Search Style (validation parsleyjs.org) + # phrase or plain or raw (websearch and trigrams are mutually exclusive) + SIMPLE = 'SIMPLE' + PLAIN = 'PLAIN' + PHRASE = 'PHRASE' + WEB = 'WEBSEARCH' + RAW = 'RAW' + SEARCH_STYLE = ( + (PLAIN, _('Plain')), + (PHRASE, _('Phrase')), + (WEB, _('Web')), + (RAW, _('Raw')) + ) + + user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True) + search = models.CharField(choices=SEARCH_STYLE, max_length=32, default=SIMPLE) + + unaccent = models.ManyToManyField(SearchFields, related_name="unaccent_fields", blank=True, default=allSearchFields) + icontains = models.ManyToManyField(SearchFields, related_name="icontains_fields", blank=True, default=nameSearchField) + istartswith = models.ManyToManyField(SearchFields, related_name="istartswith_fields", blank=True) + trigram = models.ManyToManyField(SearchFields, related_name="trigram_fields", blank=True) + fulltext = models.ManyToManyField(SearchFields, related_name="fulltext_fields", blank=True) diff --git a/cookbook/templates/settings.html b/cookbook/templates/settings.html index 8887d448..d5d66083 100644 --- a/cookbook/templates/settings.html +++ b/cookbook/templates/settings.html @@ -30,6 +30,10 @@ {% trans 'API-Settings' %} + @@ -150,6 +154,16 @@ + +