add search debug

This commit is contained in:
smilerz 2021-11-24 12:10:15 -06:00
parent 3fe5340592
commit 55a0304700
8 changed files with 812 additions and 782 deletions

1
.gitignore vendored
View File

@ -84,3 +84,4 @@ vetur.config.js
cookbook/static/vue cookbook/static/vue
vue/webpack-stats.json vue/webpack-stats.json
cookbook/templates/sw.js cookbook/templates/sw.js
.prettierignore

View File

@ -1,17 +1,17 @@
from collections import Counter from collections import Counter
from datetime import timedelta from datetime import timedelta
from recipes import settings from django.contrib.postgres.search import SearchQuery, SearchRank, TrigramSimilarity
from django.contrib.postgres.search import (
SearchQuery, SearchRank, TrigramSimilarity
)
from django.core.cache import caches 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.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils import timezone, translation from django.utils import timezone, translation
from cookbook.filters import RecipeFilter
from cookbook.helper.permission_helper import has_group_permission
from cookbook.managers import DICTIONARY from cookbook.managers import DICTIONARY
from cookbook.models import Food, Keyword, ViewLog, SearchPreference from cookbook.models import Food, Keyword, Recipe, SearchPreference, ViewLog
from recipes import settings
class Round(Func): class Round(Func):
@ -400,3 +400,13 @@ def annotated_qs(qs, root=False, fill=False):
if start_depth and start_depth > 0: if start_depth and start_depth > 0:
info['close'] = list(range(0, prev_depth - start_depth + 1)) info['close'] = list(range(0, prev_depth - start_depth + 1))
return result return result
def old_search(request):
if has_group_permission(request.user, ('guest',)):
params = dict(request.GET)
params['internal'] = None
f = RecipeFilter(params,
queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by('name'),
space=request.space)
return f.qs

View File

@ -345,6 +345,7 @@
localStorage.setItem('SCRIPT_NAME', "{% base_path request 'script' %}") localStorage.setItem('SCRIPT_NAME', "{% base_path request 'script' %}")
localStorage.setItem('BASE_PATH', "{% base_path request 'base' %}") localStorage.setItem('BASE_PATH', "{% base_path request 'base' %}")
localStorage.setItem('STATIC_URL', "{% base_path request 'static_base' %}") localStorage.setItem('STATIC_URL', "{% base_path request 'static_base' %}")
localStorage.setItem('DEBUG', "{% is_debug %}")
window.addEventListener("load", () => { window.addEventListener("load", () => {
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("{% url 'service_worker' %}", {scope: "{% base_path request 'base' %}" + '/'}).then(function (reg) { navigator.serviceWorker.register("{% url 'service_worker' %}", {scope: "{% base_path request 'base' %}" + '/'}).then(function (reg) {

View File

@ -2,17 +2,17 @@ from pydoc import locate
from django.urls import include, path from django.urls import include, path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from recipes.version import VERSION_NUMBER from rest_framework import permissions, routers
from rest_framework import routers, permissions
from rest_framework.schemas import get_schema_view from rest_framework.schemas import get_schema_view
from cookbook.helper import dal from cookbook.helper import dal
from recipes.settings import DEBUG
from recipes.version import VERSION_NUMBER
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe, from .models import (Automation, Comment, Food, InviteLink, Keyword, MealPlan, Recipe, RecipeBook,
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage, Supermarket,
Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, get_model_name, Automation, SupermarketCategory, Sync, SyncLog, Unit, UserFile, get_model_name)
UserFile, Step) from .views import api, data, delete, edit, import_export, lists, new, telegram, views
from .views import api, data, delete, edit, import_export, lists, new, views, telegram
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'user-name', api.UserNameViewSet, basename='username') router.register(r'user-name', api.UserNameViewSet, basename='username')
@ -68,8 +68,6 @@ urlpatterns = [
path('history/', views.history, name='view_history'), path('history/', views.history, name='view_history'),
path('supermarket/', views.supermarket, name='view_supermarket'), path('supermarket/', views.supermarket, name='view_supermarket'),
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'), path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
path('test/', views.test, name='view_test'),
path('test2/', views.test2, name='view_test2'),
path('import/', import_export.import_recipe, name='view_import'), path('import/', import_export.import_recipe, name='view_import'),
path('import-response/<int:pk>/', import_export.import_response, name='view_import_response'), path('import-response/<int:pk>/', import_export.import_response, name='view_import_response'),
@ -189,3 +187,7 @@ for m in vue_models:
f'list/{url_name}/', c, name=f'list_{py_name}' f'list/{url_name}/', c, name=f'list_{py_name}'
) )
) )
if DEBUG:
urlpatterns.append(path('test/', views.test, name='view_test'))
urlpatterns.append(path('test2/', views.test2, name='view_test2'))

View File

@ -2,11 +2,11 @@ import io
import json import json
import re import re
import uuid import uuid
from collections import OrderedDict
import requests import requests
from annoying.decorators import ajax_request from annoying.decorators import ajax_request
from annoying.functions import get_object_or_None from annoying.functions import get_object_or_None
from collections import OrderedDict
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.contrib.postgres.search import TrigramSimilarity
@ -15,12 +15,12 @@ from django.core.files import File
from django.db.models import Case, ProtectedError, Q, Value, When from django.db.models import Case, ProtectedError, Q, Value, When
from django.db.models.fields.related import ForeignObjectRel from django.db.models.fields.related import ForeignObjectRel
from django.http import FileResponse, HttpResponse, JsonResponse from django.http import FileResponse, HttpResponse, JsonResponse
from django_scopes import scopes_disabled from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import redirect, get_object_or_404
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from icalendar import Calendar, Event from icalendar import Calendar, Event
from recipe_scrapers import scrape_me, WebsiteNotImplementedError, NoSchemaFoundInWildMode from recipe_scrapers import NoSchemaFoundInWildMode, WebsiteNotImplementedError, scrape_me
from rest_framework import decorators, status, viewsets from rest_framework import decorators, status, viewsets
from rest_framework.exceptions import APIException, PermissionDenied from rest_framework.exceptions import APIException, PermissionDenied
from rest_framework.pagination import PageNumberPagination from rest_framework.pagination import PageNumberPagination
@ -28,41 +28,39 @@ from rest_framework.parsers import MultiPartParser
from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ViewSetMixin from rest_framework.viewsets import ViewSetMixin
from treebeard.exceptions import PathOverflow, InvalidMoveToDescendant, InvalidPosition from treebeard.exceptions import InvalidMoveToDescendant, InvalidPosition, PathOverflow
from cookbook.helper.image_processing import handle_image from cookbook.helper.image_processing import handle_image
from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, CustomIsOwner,
CustomIsOwner, CustomIsShare, CustomIsShare, CustomIsShared, CustomIsUser,
CustomIsShared, CustomIsUser,
group_required) group_required)
from cookbook.helper.recipe_html_import import get_recipe_from_source from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_search import get_facet, old_search, search_recipes
from cookbook.helper.recipe_search import search_recipes, get_facet
from cookbook.helper.recipe_url_import import get_from_scraper from cookbook.helper.recipe_url_import import get_from_scraper
from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan, from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, ImportLog, Ingredient,
MealType, Recipe, RecipeBook, ShoppingList, Keyword, MealPlan, MealType, Recipe, RecipeBook, RecipeBookEntry,
ShoppingListEntry, ShoppingListRecipe, Step, ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Step,
Storage, Sync, SyncLog, Unit, UserPreference, Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation,
ViewLog, RecipeBookEntry, Supermarket, ImportLog, BookmarkletImport, SupermarketCategory, UserFile, ShareLink, SupermarketCategoryRelation, Automation) Sync, SyncLog, Unit, UserFile, UserPreference, ViewLog)
from cookbook.provider.dropbox import Dropbox from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud from cookbook.provider.nextcloud import Nextcloud
from cookbook.schemas import FilterSchema, RecipeSchema, TreeSchema, QueryOnlySchema from cookbook.schemas import FilterSchema, QueryOnlySchema, RecipeSchema, TreeSchema
from cookbook.serializer import (FoodSerializer, IngredientSerializer, from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializer,
KeywordSerializer, MealPlanSerializer, CookLogSerializer, FoodSerializer, ImportLogSerializer,
MealTypeSerializer, RecipeBookSerializer, IngredientSerializer, KeywordSerializer, MealPlanSerializer,
RecipeImageSerializer, RecipeSerializer, MealTypeSerializer, RecipeBookEntrySerializer,
ShoppingListAutoSyncSerializer, RecipeBookSerializer, RecipeImageSerializer,
ShoppingListEntrySerializer, RecipeOverviewSerializer, RecipeSerializer,
ShoppingListRecipeSerializer, ShoppingListAutoSyncSerializer, ShoppingListEntrySerializer,
ShoppingListSerializer, StepSerializer, ShoppingListRecipeSerializer, ShoppingListSerializer,
StorageSerializer, SyncLogSerializer, StepSerializer, StorageSerializer,
SyncSerializer, UnitSerializer, SupermarketCategoryRelationSerializer,
UserNameSerializer, UserPreferenceSerializer, SupermarketCategorySerializer, SupermarketSerializer,
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer, SyncLogSerializer, SyncSerializer, UnitSerializer,
RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer, UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
BookmarkletImportSerializer, SupermarketCategorySerializer, UserFileSerializer, SupermarketCategoryRelationSerializer, AutomationSerializer) ViewLogSerializer)
from recipes import settings from recipes import settings
@ -547,7 +545,16 @@ class RecipeViewSet(viewsets.ModelViewSet):
return super().get_queryset() return super().get_queryset()
def list(self, request, *args, **kwargs):
if self.request.GET.get('debug', False):
return JsonResponse({
'new': str(self.get_queryset().query),
'old': str(old_search(request).query)
})
return super().list(request, *args, **kwargs)
# TODO write extensive tests for permissions # TODO write extensive tests for permissions
def get_serializer_class(self): def get_serializer_class(self):
if self.action == 'list': if self.action == 'list':
return RecipeOverviewSerializer return RecipeOverviewSerializer

View File

@ -13,7 +13,7 @@ from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Avg, Q, Sum from django.db.models import Avg, Q, Sum
from django.http import HttpResponseRedirect, JsonResponse from django.http import HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -22,16 +22,15 @@ from django_tables2 import RequestConfig
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from cookbook.filters import RecipeFilter from cookbook.filters import RecipeFilter
from cookbook.forms import (CommentForm, Recipe, User, from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, SpaceCreateForm,
UserCreateForm, UserNameForm, UserPreference, SpaceJoinForm, User, UserCreateForm, UserNameForm, UserPreference,
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm, UserPreferenceForm)
SearchPreferenceForm) from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid
from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission from cookbook.models import (Comment, CookLog, Food, InviteLink, Keyword, MealPlan, RecipeImport,
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan, SearchFields, SearchPreference, ShareLink, ShoppingList, Space, Unit,
ViewLog, ShoppingList, Space, Keyword, RecipeImport, Unit, UserFile, ViewLog)
Food, UserFile, ShareLink, SearchPreference, SearchFields) from cookbook.tables import (CookLogTable, InviteLinkTable, RecipeTable, RecipeTableSmall,
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall, ViewLogTable)
ViewLogTable, InviteLinkTable)
from cookbook.views.data import Object from cookbook.views.data import Object
from recipes.version import BUILD_REF, VERSION_NUMBER from recipes.version import BUILD_REF, VERSION_NUMBER

View File

@ -7,140 +7,100 @@
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-12 col-lg-10 col-xl-8 mt-3 mb-3"> <div class="col-12 col-lg-10 col-xl-8 mt-3 mb-3">
<b-input-group> <b-input-group>
<b-input class="form-control form-control-lg form-control-borderless form-control-search" v-model="settings.search_input" <b-input
v-bind:placeholder="$t('Search')"></b-input> class="form-control form-control-lg form-control-borderless form-control-search"
v-model="settings.search_input"
v-bind:placeholder="$t('Search')"
></b-input>
<b-input-group-append> <b-input-group-append>
<b-button variant="light" <b-button v-b-tooltip.hover :title="$t('show_sql')" @click="showSQL()">
v-b-tooltip.hover :title="$t('Random Recipes')" <i class="fas fa-bug" style="font-size: 1.5em"></i>
@click="openRandom()"> </b-button>
<b-button variant="light" v-b-tooltip.hover :title="$t('Random Recipes')" @click="openRandom()">
<i class="fas fa-dice-five" style="font-size: 1.5em"></i> <i class="fas fa-dice-five" style="font-size: 1.5em"></i>
</b-button> </b-button>
<b-button v-b-toggle.collapse_advanced_search <b-button
v-b-tooltip.hover :title="$t('Advanced Settings')" v-b-toggle.collapse_advanced_search
v-b-tooltip.hover
:title="$t('Advanced Settings')"
v-bind:variant="!isAdvancedSettingsSet() ? 'primary' : 'danger'" v-bind:variant="!isAdvancedSettingsSet() ? 'primary' : 'danger'"
> >
<!-- TODO consider changing this icon to a filter --> <!-- TODO consider changing this icon to a filter -->
<i class="fas fa-caret-down" v-if="!settings.advanced_search_visible"></i> <i class="fas fa-caret-down" v-if="!settings.advanced_search_visible"></i>
<i class="fas fa-caret-up" v-if="settings.advanced_search_visible"></i> <i class="fas fa-caret-up" v-if="settings.advanced_search_visible"></i>
</b-button> </b-button>
</b-input-group-append> </b-input-group-append>
</b-input-group> </b-input-group>
</div> </div>
</div> </div>
<b-collapse id="collapse_advanced_search" class="mt-2 shadow-sm" v-model="settings.advanced_search_visible"> <b-collapse id="collapse_advanced_search" class="mt-2 shadow-sm" v-model="settings.advanced_search_visible">
<div class="card"> <div class="card">
<div class="card-body p-4"> <div class="card-body p-4">
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
<a class="btn btn-primary btn-block text-uppercase" <a class="btn btn-primary btn-block text-uppercase" :href="resolveDjangoUrl('new_recipe')">{{ $t("New_Recipe") }}</a>
:href="resolveDjangoUrl('new_recipe')">{{ $t('New_Recipe') }}</a>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<a class="btn btn-primary btn-block text-uppercase" <a class="btn btn-primary btn-block text-uppercase" :href="resolveDjangoUrl('data_import_url')">{{ $t("Import") }}</a>
:href="resolveDjangoUrl('data_import_url')">{{ $t('Import') }}</a>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<button class="btn btn-block text-uppercase" v-b-tooltip.hover :title="$t('show_only_internal')" <button
class="btn btn-block text-uppercase"
v-b-tooltip.hover
:title="$t('show_only_internal')"
v-bind:class="{ 'btn-success': settings.search_internal, 'btn-primary': !settings.search_internal }" v-bind:class="{ 'btn-success': settings.search_internal, 'btn-primary': !settings.search_internal }"
@click="settings.search_internal = !settings.search_internal;refreshData()"> @click="
{{ $t('Internal') }} settings.search_internal = !settings.search_internal
refreshData()
"
>
{{ $t("Internal") }}
</button> </button>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<button id="id_settings_button" class="btn btn-primary btn-block text-uppercase"><i <button id="id_settings_button" class="btn btn-primary btn-block text-uppercase"><i class="fas fa-cog fa-lg m-1"></i></button>
class="fas fa-cog fa-lg m-1"></i> </div>
</button>
</div> </div>
<b-popover target="id_settings_button" triggers="click" placement="bottom" :title="$t('Settings')">
</div>
<b-popover
target="id_settings_button"
triggers="click"
placement="bottom"
:title="$t('Settings')">
<div> <div>
<b-form-group <b-form-group v-bind:label="$t('Recently_Viewed')" label-for="popover-input-1" label-cols="6" class="mb-3">
v-bind:label="$t('Recently_Viewed')" <b-form-input type="number" v-model="settings.recently_viewed" id="popover-input-1" size="sm"></b-form-input>
label-for="popover-input-1" </b-form-group>
label-cols="6"
class="mb-3"> <b-form-group v-bind:label="$t('Recipes_per_page')" label-for="popover-input-page-count" label-cols="6" class="mb-3">
<b-form-input <b-form-input type="number" v-model="settings.page_count" id="popover-input-page-count" size="sm"></b-form-input>
type="number" </b-form-group>
v-model="settings.recently_viewed"
id="popover-input-1" <b-form-group v-bind:label="$t('Meal_Plan')" label-for="popover-input-2" label-cols="6" class="mb-3">
size="sm" <b-form-checkbox switch v-model="settings.show_meal_plan" id="popover-input-2" size="sm"></b-form-checkbox>
></b-form-input>
</b-form-group> </b-form-group>
<b-form-group <b-form-group
v-bind:label="$t('Recipes_per_page')" v-if="settings.show_meal_plan"
label-for="popover-input-page-count"
label-cols="6"
class="mb-3">
<b-form-input
type="number"
v-model="settings.page_count"
id="popover-input-page-count"
size="sm"
></b-form-input>
</b-form-group>
<b-form-group
v-bind:label="$t('Meal_Plan')"
label-for="popover-input-2"
label-cols="6"
class="mb-3">
<b-form-checkbox
switch
v-model="settings.show_meal_plan"
id="popover-input-2"
size="sm"
></b-form-checkbox>
</b-form-group>
<b-form-group v-if="settings.show_meal_plan"
v-bind:label="$t('Meal_Plan_Days')" v-bind:label="$t('Meal_Plan_Days')"
label-for="popover-input-5" label-for="popover-input-5"
label-cols="6" label-cols="6"
class="mb-3"> class="mb-3"
<b-form-input >
type="number" <b-form-input type="number" v-model="settings.meal_plan_days" id="popover-input-5" size="sm"></b-form-input>
v-model="settings.meal_plan_days"
id="popover-input-5"
size="sm"
></b-form-input>
</b-form-group> </b-form-group>
<b-form-group <b-form-group v-bind:label="$t('Sort_by_new')" label-for="popover-input-3" label-cols="6" class="mb-3">
v-bind:label="$t('Sort_by_new')" <b-form-checkbox switch v-model="settings.sort_by_new" id="popover-input-3" size="sm"></b-form-checkbox>
label-for="popover-input-3"
label-cols="6"
class="mb-3">
<b-form-checkbox
switch
v-model="settings.sort_by_new"
id="popover-input-3"
size="sm"
></b-form-checkbox>
</b-form-group> </b-form-group>
</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') + '#search'">{{ $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">
<div class="col-12" style="text-align: right"> <div class="col-12" style="text-align: right">
<b-button size="sm" variant="secondary" style="margin-right:8px" <b-button size="sm" variant="secondary" style="margin-right: 8px" @click="$root.$emit('bv::hide::popover')"
@click="$root.$emit('bv::hide::popover')">{{ $t('Close') }} >{{ $t("Close") }}
</b-button> </b-button>
</div> </div>
</div> </div>
@ -150,17 +110,28 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<b-input-group class="mt-2"> <b-input-group class="mt-2">
<treeselect v-model="settings.search_keywords" :options="facets.Keywords" :flat="true" <treeselect
searchNested multiple :placeholder="$t('Keywords')" :normalizer="normalizer" v-model="settings.search_keywords"
:options="facets.Keywords"
:flat="true"
searchNested
multiple
:placeholder="$t('Keywords')"
:normalizer="normalizer"
@input="refreshData(false)" @input="refreshData(false)"
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> <b-input-group-text>
<b-form-checkbox v-model="settings.search_keywords_or" name="check-button" <b-form-checkbox
v-model="settings.search_keywords_or"
name="check-button"
@change="refreshData(false)" @change="refreshData(false)"
class="shadow-none" switch> class="shadow-none"
<span class="text-uppercase" v-if="settings.search_keywords_or">{{ $t('or') }}</span> switch
<span class="text-uppercase" v-else>{{ $t('and') }}</span> >
<span class="text-uppercase" v-if="settings.search_keywords_or">{{ $t("or") }}</span>
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
</b-form-checkbox> </b-form-checkbox>
</b-input-group-text> </b-input-group-text>
</b-input-group-append> </b-input-group-append>
@ -172,17 +143,28 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<b-input-group class="mt-2"> <b-input-group class="mt-2">
<treeselect v-model="settings.search_foods" :options="facets.Foods" :flat="true" <treeselect
searchNested multiple :placeholder="$t('Ingredients')" :normalizer="normalizer" v-model="settings.search_foods"
:options="facets.Foods"
:flat="true"
searchNested
multiple
:placeholder="$t('Ingredients')"
:normalizer="normalizer"
@input="refreshData(false)" @input="refreshData(false)"
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> <b-input-group-text>
<b-form-checkbox v-model="settings.search_foods_or" name="check-button" <b-form-checkbox
v-model="settings.search_foods_or"
name="check-button"
@change="refreshData(false)" @change="refreshData(false)"
class="shadow-none" switch> class="shadow-none"
<span class="text-uppercase" v-if="settings.search_foods_or">{{ $t('or') }}</span> switch
<span class="text-uppercase" v-else>{{ $t('and') }}</span> >
<span class="text-uppercase" v-if="settings.search_foods_or">{{ $t("or") }}</span>
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
</b-form-checkbox> </b-form-checkbox>
</b-input-group-text> </b-input-group-text>
</b-input-group-append> </b-input-group-append>
@ -194,18 +176,27 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<b-input-group class="mt-2"> <b-input-group class="mt-2">
<generic-multiselect @change="genericSelectChanged" parent_variable="search_books" <generic-multiselect
@change="genericSelectChanged"
parent_variable="search_books"
:initial_selection="settings.search_books" :initial_selection="settings.search_books"
:model="Models.RECIPE_BOOK" :model="Models.RECIPE_BOOK"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0" style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Books')" :limit="50"></generic-multiselect> v-bind:placeholder="$t('Books')"
:limit="50"
></generic-multiselect>
<b-input-group-append> <b-input-group-append>
<b-input-group-text> <b-input-group-text>
<b-form-checkbox v-model="settings.search_books_or" name="check-button" <b-form-checkbox
v-model="settings.search_books_or"
name="check-button"
@change="refreshData(false)" @change="refreshData(false)"
class="shadow-none" tyle="width: 100%" switch> class="shadow-none"
<span class="text-uppercase" v-if="settings.search_books_or">{{ $t('or') }}</span> tyle="width: 100%"
<span class="text-uppercase" v-else>{{ $t('and') }}</span> 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-form-checkbox>
</b-input-group-text> </b-input-group-text>
</b-input-group-append> </b-input-group-append>
@ -217,101 +208,93 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<b-input-group class="mt-2"> <b-input-group class="mt-2">
<treeselect v-model="settings.search_ratings" :options="ratingOptions" :flat="true" <treeselect
:placeholder="$t('Ratings')" :searchable="false" v-model="settings.search_ratings"
:options="ratingOptions"
:flat="true"
:placeholder="$t('Ratings')"
:searchable="false"
@input="refreshData(false)" @input="refreshData(false)"
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-input-group-text>
</b-input-group-text>
</b-input-group-append> </b-input-group-append>
</b-input-group> </b-input-group>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</b-collapse> </b-collapse>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col col-md-12 text-right" style="margin-top: 2vh"> <div class="col col-md-12 text-right" style="margin-top: 2vh">
<span class="text-muted"> <span class="text-muted">
{{ $t('Page') }} {{ settings.pagination_page }}/{{ Math.ceil(pagination_count/settings.page_count) }} <a href="#" @click="resetSearch"><i {{ $t("Page") }} {{ settings.pagination_page }}/{{ Math.ceil(pagination_count / settings.page_count) }}
class="fas fa-times-circle"></i> {{ $t('Reset') }}</a> <a href="#" @click="resetSearch"><i class="fas fa-times-circle"></i> {{ $t("Reset") }}</a>
</span> </span>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 0.8rem;" > <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 0.8rem">
<template v-if="!searchFiltered"> <template v-if="!searchFiltered">
<recipe-card v-bind:key="`mp_${m.id}`" v-for="m in meal_plans" :recipe="m.recipe" <recipe-card
:meal_plan="m" :footer_text="m.meal_type_name" v-bind:key="`mp_${m.id}`"
footer_icon="far fa-calendar-alt"></recipe-card> v-for="m in meal_plans"
:recipe="m.recipe"
:meal_plan="m"
:footer_text="m.meal_type_name"
footer_icon="far fa-calendar-alt"
></recipe-card>
</template> </template>
<recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r" <recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r" :footer_text="isRecentOrNew(r)[0]" :footer_icon="isRecentOrNew(r)[1]"> </recipe-card>
:footer_text="isRecentOrNew(r)[0]"
:footer_icon="isRecentOrNew(r)[1]">
</recipe-card>
</div> </div>
</div> </div>
</div> </div>
<div class="row" style="margin-top: 2vh" v-if="!random_search"> <div class="row" style="margin-top: 2vh" v-if="!random_search">
<div class="col col-md-12"> <div class="col col-md-12">
<b-pagination pills <b-pagination pills v-model="settings.pagination_page" :total-rows="pagination_count" :per-page="settings.page_count" @change="pageChange" align="center">
v-model="settings.pagination_page"
:total-rows="pagination_count"
:per-page="settings.page_count"
@change="pageChange"
align="center">
</b-pagination> </b-pagination>
</div> </div>
</div> </div>
</div>
<div class="col-md-2 d-none d-md-block">
</div> </div>
<div class="col-md-2 d-none d-md-block"></div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Vue from 'vue' import Vue from "vue"
import {BootstrapVue} from 'bootstrap-vue' import { BootstrapVue } from "bootstrap-vue"
import 'bootstrap-vue/dist/bootstrap-vue.css' import "bootstrap-vue/dist/bootstrap-vue.css"
import moment from 'moment' import moment from "moment"
import _debounce from 'lodash/debounce' import _debounce from "lodash/debounce"
import VueCookies from 'vue-cookies' import VueCookies from "vue-cookies"
Vue.use(VueCookies) Vue.use(VueCookies)
import {ApiMixin, ResolveUrlMixin} from "@/utils/utils"; import { ApiMixin, ResolveUrlMixin } from "@/utils/utils"
import LoadingSpinner from "@/components/LoadingSpinner"; // is this deprecated? import LoadingSpinner from "@/components/LoadingSpinner" // is this deprecated?
import RecipeCard from "@/components/RecipeCard"; import RecipeCard from "@/components/RecipeCard"
import GenericMultiselect from "@/components/GenericMultiselect"; import GenericMultiselect from "@/components/GenericMultiselect"
import Treeselect from '@riophae/vue-treeselect' import Treeselect from "@riophae/vue-treeselect"
import '@riophae/vue-treeselect/dist/vue-treeselect.css' import "@riophae/vue-treeselect/dist/vue-treeselect.css"
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
let SETTINGS_COOKIE_NAME = 'search_settings' let SETTINGS_COOKIE_NAME = "search_settings"
export default { export default {
name: 'RecipeSearchView', name: "RecipeSearchView",
mixins: [ResolveUrlMixin, ApiMixin], mixins: [ResolveUrlMixin, ApiMixin],
components: { GenericMultiselect, RecipeCard, Treeselect }, components: { GenericMultiselect, RecipeCard, Treeselect },
data() { data() {
@ -324,7 +307,7 @@ export default {
settings_loaded: false, settings_loaded: false,
settings: { settings: {
search_input: '', search_input: "",
search_internal: false, search_internal: false,
search_keywords: [], search_keywords: [],
search_foods: [], search_foods: [],
@ -344,30 +327,30 @@ export default {
}, },
pagination_count: 0, pagination_count: 0,
random_search: false random_search: false,
debug: false,
} }
}, },
computed: { computed: {
ratingOptions: function () { ratingOptions: function () {
return [ return [
{'id': 5, 'label': '⭐⭐⭐⭐⭐' + ' (' + (this.facets.Ratings?.['5.0'] ?? 0) + ')' }, { id: 5, label: "⭐⭐⭐⭐⭐" + " (" + (this.facets.Ratings?.["5.0"] ?? 0) + ")" },
{'id': 4, 'label': '⭐⭐⭐⭐ ' + this.$t('and_up') + ' (' + (this.facets.Ratings?.['4.0'] ?? 0) + ')' }, { id: 4, label: "⭐⭐⭐⭐ " + this.$t("and_up") + " (" + (this.facets.Ratings?.["4.0"] ?? 0) + ")" },
{'id': 3, 'label': '⭐⭐⭐ ' + this.$t('and_up') + ' (' + (this.facets.Ratings?.['3.0'] ?? 0) + ')' }, { id: 3, label: "⭐⭐⭐ " + this.$t("and_up") + " (" + (this.facets.Ratings?.["3.0"] ?? 0) + ")" },
{'id': 2, 'label': '⭐⭐ ' + this.$t('and_up') + ' (' + (this.facets.Ratings?.['2.0'] ?? 0) + ')' }, { id: 2, label: "⭐⭐ " + this.$t("and_up") + " (" + (this.facets.Ratings?.["2.0"] ?? 0) + ")" },
{'id': 1, 'label': '⭐ ' + this.$t("and_up") + ' (' + (this.facets.Ratings?.['1.0'] ?? 0) + ')' }, { id: 1, label: "⭐ " + this.$t("and_up") + " (" + (this.facets.Ratings?.["1.0"] ?? 0) + ")" },
{'id': -1, 'label': this.$t('Unrated') + ' (' + (this.facets.Ratings?.['0.0'] ?? 0 )+ ')'}, { id: -1, label: this.$t("Unrated") + " (" + (this.facets.Ratings?.["0.0"] ?? 0) + ")" },
] ]
}, },
searchFiltered: function () { searchFiltered: function () {
if ( if (
this.settings?.search_input === '' this.settings?.search_input === "" &&
&& this.settings?.search_keywords?.length === 0 this.settings?.search_keywords?.length === 0 &&
&& this.settings?.search_foods?.length === 0 this.settings?.search_foods?.length === 0 &&
&& this.settings?.search_books?.length === 0 this.settings?.search_books?.length === 0 &&
&& this.settings?.pagination_page === 1 this.settings?.pagination_page === 1 &&
&& !this.random_search !this.random_search &&
&& this.settings?.search_ratings === undefined this.settings?.search_ratings === undefined
) { ) {
return false return false
} else { } else {
@ -380,55 +363,56 @@ 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")) {
this.settings.search_keywords = [] this.settings.search_keywords = []
this.facets.Keywords = [] this.facets.Keywords = []
for (let x of urlParams.getAll('keyword')) { for (let x of urlParams.getAll("keyword")) {
this.settings.search_keywords.push(Number.parseInt(x)) this.settings.search_keywords.push(Number.parseInt(x))
this.facets.Keywords.push({'id':x, 'name': 'loading...'}) this.facets.Keywords.push({ id: x, name: "loading..." })
} }
} }
this.facets.Foods = [] this.facets.Foods = []
for (let x of this.settings.search_foods) { for (let x of this.settings.search_foods) {
this.facets.Foods.push({'id':x, 'name': 'loading...'}) this.facets.Foods.push({ id: x, name: "loading..." })
} }
this.facets.Keywords = [] this.facets.Keywords = []
for (let x of this.settings.search_keywords) { for (let x of this.settings.search_keywords) {
this.facets.Keywords.push({'id':x, 'name': 'loading...'}) this.facets.Keywords.push({ id: x, name: "loading..." })
} }
this.facets.Books = [] this.facets.Books = []
for (let x of this.settings.search_books) { for (let x of this.settings.search_books) {
this.facets.Books.push({'id':x, 'name': 'loading...'}) this.facets.Books.push({ id: x, name: "loading..." })
} }
this.loadMealPlan() this.loadMealPlan()
this.refreshData(false) this.refreshData(false)
}) })
this.$i18n.locale = window.CUSTOM_LOCALE this.$i18n.locale = window.CUSTOM_LOCALE
this.debug = localStorage.getItem("DEBUG") || false
}, },
watch: { watch: {
settings: { settings: {
handler() { handler() {
this.$cookies.set(SETTINGS_COOKIE_NAME, this.settings, '4h') this.$cookies.set(SETTINGS_COOKIE_NAME, this.settings, "4h")
}, },
deep: true deep: true,
}, },
'settings.show_meal_plan': function () { "settings.show_meal_plan": function () {
this.loadMealPlan() this.loadMealPlan()
}, },
'settings.meal_plan_days': function () { "settings.meal_plan_days": function () {
this.loadMealPlan() this.loadMealPlan()
}, },
'settings.recently_viewed': function () { "settings.recently_viewed": function () {
this.refreshData(false) this.refreshData(false)
}, },
'settings.search_input': _debounce(function () { "settings.search_input": _debounce(function () {
this.settings.pagination_page = 1 this.settings.pagination_page = 1
this.pagination_count = 0 this.pagination_count = 0
this.refreshData(false) this.refreshData(false)
}, 300), }, 300),
'settings.page_count': _debounce(function () { "settings.page_count": _debounce(function () {
this.refreshData(false) this.refreshData(false)
}, 300), }, 300),
}, },
@ -437,39 +421,39 @@ export default {
refreshData: function (random) { refreshData: function (random) {
this.random_search = random this.random_search = random
let params = { let params = {
'query': this.settings.search_input, query: this.settings.search_input,
'keywords': this.settings.search_keywords, keywords: this.settings.search_keywords,
'foods': this.settings.search_foods, foods: this.settings.search_foods,
'rating': this.settings.search_ratings, rating: this.settings.search_ratings,
'books': this.settings.search_books.map(function (A) { books: this.settings.search_books.map(function (A) {
return A["id"]; return A["id"]
}), }),
'keywordsOr': this.settings.search_keywords_or, keywordsOr: this.settings.search_keywords_or,
'foodsOr': this.settings.search_foods_or, foodsOr: this.settings.search_foods_or,
'booksOr': this.settings.search_books_or, booksOr: this.settings.search_books_or,
'internal': this.settings.search_internal, internal: this.settings.search_internal,
'random': this.random_search, random: this.random_search,
'_new': this.settings.sort_by_new, _new: this.settings.sort_by_new,
'page': this.settings.pagination_page, page: this.settings.pagination_page,
'pageSize': this.settings.page_count pageSize: this.settings.page_count,
} }
if (!this.searchFiltered) { if (!this.searchFiltered) {
params.options = {'query':{'last_viewed': this.settings.recently_viewed}} params.options = { query: { last_viewed: this.settings.recently_viewed } }
} }
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then(result => { this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {
window.scrollTo(0, 0); window.scrollTo(0, 0)
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) { if (this.facets?.cache_key) {
this.getFacets(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
let mealPlans = [] let mealPlans = []
this.meal_plans.forEach(x => mealPlans.push(x.recipe.id)) this.meal_plans.forEach((x) => mealPlans.push(x.recipe.id))
this.recipes = this.recipes.filter(recipe => !mealPlans.includes(recipe.id)) this.recipes = this.recipes.filter((recipe) => !mealPlans.includes(recipe.id))
} }
}) })
}, },
@ -477,19 +461,19 @@ export default {
this.refreshData(true) this.refreshData(true)
}, },
removeDuplicates: function (data, key) { removeDuplicates: function (data, key) {
return [ return [...new Map(data.map((item) => [key(item), item])).values()]
...new Map(data.map(item => [key(item), item])).values()
]
}, },
loadMealPlan: function () { loadMealPlan: function () {
if (this.settings.show_meal_plan) { if (this.settings.show_meal_plan) {
let params = { let params = {
'options': {'query':{ options: {
'from_date': moment().format('YYYY-MM-DD'), query: {
'to_date': moment().add(this.settings.meal_plan_days, 'days').format('YYYY-MM-DD') from_date: moment().format("YYYY-MM-DD"),
}} to_date: moment().add(this.settings.meal_plan_days, "days").format("YYYY-MM-DD"),
},
},
} }
this.genericAPI(this.Models.MEAL_PLAN, this.Actions.LIST, params).then(result => { this.genericAPI(this.Models.MEAL_PLAN, this.Actions.LIST, params).then((result) => {
this.meal_plans = result.data this.meal_plans = result.data
}) })
} else { } else {
@ -501,7 +485,7 @@ export default {
this.refreshData(false) this.refreshData(false)
}, },
resetSearch: function () { resetSearch: function () {
this.settings.search_input = '' this.settings.search_input = ""
this.settings.search_internal = false this.settings.search_internal = false
this.settings.search_keywords = [] this.settings.search_keywords = []
this.settings.search_foods = [] this.settings.search_foods = []
@ -515,20 +499,20 @@ export default {
this.refreshData(false) this.refreshData(false)
}, },
isAdvancedSettingsSet() { isAdvancedSettingsSet() {
return ((this.settings.search_keywords.length + this.settings.search_foods.length + this.settings.search_books.length) > 0) return this.settings.search_keywords.length + this.settings.search_foods.length + this.settings.search_books.length > 0
}, },
normalizer(node) { normalizer(node) {
let count = (node?.count ? ' (' + node.count + ')' : '') let count = node?.count ? " (" + node.count + ")" : ""
return { return {
id: node.id, id: node.id,
label: node.name + count, label: node.name + count,
children: node.children, children: node.children,
isDefaultExpanded: node.isDefaultExpanded isDefaultExpanded: node.isDefaultExpanded,
} }
}, },
isRecentOrNew: function (x) { isRecentOrNew: function (x) {
let recent_recipe = [this.$t('Recently_Viewed'), "fas fa-eye"] let recent_recipe = [this.$t("Recently_Viewed"), "fas fa-eye"]
let new_recipe = [this.$t('New_Recipe'), "fas fa-splotch"] let new_recipe = [this.$t("New_Recipe"), "fas fa-splotch"]
if (x.new) { if (x.new) {
return new_recipe return new_recipe
} else if (this.facets.Recent.includes(x.id)) { } else if (this.facets.Recent.includes(x.id)) {
@ -538,17 +522,42 @@ export default {
} }
}, },
getFacets: function (hash) { getFacets: function (hash) {
this.genericGetAPI('api_get_facets', {hash: hash}).then((response) => { this.genericGetAPI("api_get_facets", { hash: hash }).then((response) => {
this.facets = { ...this.facets, ...response.data.facets } this.facets = { ...this.facets, ...response.data.facets }
}) })
},
showSQL: function () {
// TODO refactor this so that it isn't a total copy of refreshData
let params = {
query: this.settings.search_input,
keywords: this.settings.search_keywords,
foods: this.settings.search_foods,
rating: this.settings.search_ratings,
books: this.settings.search_books.map(function (A) {
return A["id"]
}),
keywordsOr: this.settings.search_keywords_or,
foodsOr: this.settings.search_foods_or,
booksOr: this.settings.search_books_or,
internal: this.settings.search_internal,
random: this.random_search,
_new: this.settings.sort_by_new,
page: this.settings.pagination_page,
pageSize: this.settings.page_count,
} }
if (!this.searchFiltered) {
params.options = { query: { last_viewed: this.settings.recently_viewed, debug: true } }
} else {
params.options = { query: { debug: true } }
} }
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {
console.log(result.data)
})
},
},
} }
</script> </script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style> <style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
<style> <style></style>
</style>

View File

@ -206,5 +206,6 @@
"Auto_Planner": "Auto-Planner", "Auto_Planner": "Auto-Planner",
"New_Cookbook": "New cookbook", "New_Cookbook": "New cookbook",
"Hide_Keyword": "Hide keywords", "Hide_Keyword": "Hide keywords",
"Clear": "Clear" "Clear": "Clear",
"show_sql": "Show SQL"
} }