Merge pull request #1088 from smilerz/search_troubleshooting

add search debug
This commit is contained in:
vabene1111 2021-11-30 17:21:40 +01:00 committed by GitHub
commit 7c985cec23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 812 additions and 782 deletions

1
.gitignore vendored
View File

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

View File

@ -1,17 +1,17 @@
from collections import Counter
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.db.models import Avg, Case, Count, Func, Max, Q, Subquery, Value, When
from django.db.models.functions import Coalesce
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.models import Food, Keyword, ViewLog, SearchPreference
from cookbook.models import Food, Keyword, Recipe, SearchPreference, ViewLog
from recipes import settings
class Round(Func):
@ -400,3 +400,13 @@ def annotated_qs(qs, root=False, fill=False):
if start_depth and start_depth > 0:
info['close'] = list(range(0, prev_depth - start_depth + 1))
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('BASE_PATH', "{% base_path request 'base' %}")
localStorage.setItem('STATIC_URL', "{% base_path request 'static_base' %}")
localStorage.setItem('DEBUG', "{% is_debug %}")
window.addEventListener("load", () => {
if ("serviceWorker" in navigator) {
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.views.generic import TemplateView
from recipes.version import VERSION_NUMBER
from rest_framework import routers, permissions
from rest_framework import permissions, routers
from rest_framework.schemas import get_schema_view
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,
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList,
Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, get_model_name, Automation,
UserFile, Step)
from .views import api, data, delete, edit, import_export, lists, new, views, telegram
from .models import (Automation, Comment, Food, InviteLink, Keyword, MealPlan, Recipe, RecipeBook,
RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage, Supermarket,
SupermarketCategory, Sync, SyncLog, Unit, UserFile, get_model_name)
from .views import api, data, delete, edit, import_export, lists, new, telegram, views
router = routers.DefaultRouter()
router.register(r'user-name', api.UserNameViewSet, basename='username')
@ -68,8 +68,6 @@ urlpatterns = [
path('history/', views.history, name='view_history'),
path('supermarket/', views.supermarket, name='view_supermarket'),
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-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}'
)
)
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 re
import uuid
from collections import OrderedDict
import requests
from annoying.decorators import ajax_request
from annoying.functions import get_object_or_None
from collections import OrderedDict
from django.contrib import messages
from django.contrib.auth.models import User
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.fields.related import ForeignObjectRel
from django.http import FileResponse, HttpResponse, JsonResponse
from django_scopes import scopes_disabled
from django.shortcuts import redirect, get_object_or_404
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
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.exceptions import APIException, PermissionDenied
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.response import Response
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.ingredient_parser import IngredientParser
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
CustomIsOwner, CustomIsShare,
CustomIsShared, CustomIsUser,
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, CustomIsOwner,
CustomIsShare, CustomIsShared, CustomIsUser,
group_required)
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_search import search_recipes, get_facet
from cookbook.helper.recipe_search import get_facet, old_search, search_recipes
from cookbook.helper.recipe_url_import import get_from_scraper
from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
MealType, Recipe, RecipeBook, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Step,
Storage, Sync, SyncLog, Unit, UserPreference,
ViewLog, RecipeBookEntry, Supermarket, ImportLog, BookmarkletImport, SupermarketCategory, UserFile, ShareLink, SupermarketCategoryRelation, Automation)
from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, ImportLog, Ingredient,
Keyword, MealPlan, MealType, Recipe, RecipeBook, RecipeBookEntry,
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Step,
Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation,
Sync, SyncLog, Unit, UserFile, UserPreference, ViewLog)
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
from cookbook.schemas import FilterSchema, RecipeSchema, TreeSchema, QueryOnlySchema
from cookbook.serializer import (FoodSerializer, IngredientSerializer,
KeywordSerializer, MealPlanSerializer,
MealTypeSerializer, RecipeBookSerializer,
RecipeImageSerializer, RecipeSerializer,
ShoppingListAutoSyncSerializer,
ShoppingListEntrySerializer,
ShoppingListRecipeSerializer,
ShoppingListSerializer, StepSerializer,
StorageSerializer, SyncLogSerializer,
SyncSerializer, UnitSerializer,
UserNameSerializer, UserPreferenceSerializer,
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer,
RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer,
BookmarkletImportSerializer, SupermarketCategorySerializer, UserFileSerializer, SupermarketCategoryRelationSerializer, AutomationSerializer)
from cookbook.schemas import FilterSchema, QueryOnlySchema, RecipeSchema, TreeSchema
from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializer,
CookLogSerializer, FoodSerializer, ImportLogSerializer,
IngredientSerializer, KeywordSerializer, MealPlanSerializer,
MealTypeSerializer, RecipeBookEntrySerializer,
RecipeBookSerializer, RecipeImageSerializer,
RecipeOverviewSerializer, RecipeSerializer,
ShoppingListAutoSyncSerializer, ShoppingListEntrySerializer,
ShoppingListRecipeSerializer, ShoppingListSerializer,
StepSerializer, StorageSerializer,
SupermarketCategoryRelationSerializer,
SupermarketCategorySerializer, SupermarketSerializer,
SyncLogSerializer, SyncSerializer, UnitSerializer,
UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
ViewLogSerializer)
from recipes import settings
@ -547,7 +545,16 @@ class RecipeViewSet(viewsets.ModelViewSet):
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
def get_serializer_class(self):
if self.action == 'list':
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.db.models import Avg, Q, Sum
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.utils import timezone
from django.utils.translation import gettext as _
@ -22,16 +22,15 @@ from django_tables2 import RequestConfig
from rest_framework.authtoken.models import Token
from cookbook.filters import RecipeFilter
from cookbook.forms import (CommentForm, Recipe, User,
UserCreateForm, UserNameForm, UserPreference,
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm,
SearchPreferenceForm)
from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
ViewLog, ShoppingList, Space, Keyword, RecipeImport, Unit,
Food, UserFile, ShareLink, SearchPreference, SearchFields)
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall,
ViewLogTable, InviteLinkTable)
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, SpaceCreateForm,
SpaceJoinForm, User, UserCreateForm, UserNameForm, UserPreference,
UserPreferenceForm)
from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid
from cookbook.models import (Comment, CookLog, Food, InviteLink, Keyword, MealPlan, RecipeImport,
SearchFields, SearchPreference, ShareLink, ShoppingList, Space, Unit,
UserFile, ViewLog)
from cookbook.tables import (CookLogTable, InviteLinkTable, RecipeTable, RecipeTableSmall,
ViewLogTable)
from cookbook.views.data import Object
from recipes.version import BUILD_REF, VERSION_NUMBER
@ -382,7 +381,7 @@ def user_settings(request):
if up:
preference_form = UserPreferenceForm(instance=up, space=request.space)
else:
preference_form = UserPreferenceForm( space=request.space)
preference_form = UserPreferenceForm(space=request.space)
fields_searched = len(sp.icontains.all()) + len(sp.istartswith.all()) + len(sp.trigram.all()) + len(
sp.fulltext.all())

View File

@ -7,140 +7,100 @@
<div class="row justify-content-center">
<div class="col-12 col-lg-10 col-xl-8 mt-3 mb-3">
<b-input-group>
<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
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-button variant="light"
v-b-tooltip.hover :title="$t('Random Recipes')"
@click="openRandom()">
<b-button v-b-tooltip.hover :title="$t('show_sql')" @click="showSQL()">
<i class="fas fa-bug" style="font-size: 1.5em"></i>
</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>
</b-button>
<b-button v-b-toggle.collapse_advanced_search
v-b-tooltip.hover :title="$t('Advanced Settings')"
<b-button
v-b-toggle.collapse_advanced_search
v-b-tooltip.hover
:title="$t('Advanced Settings')"
v-bind:variant="!isAdvancedSettingsSet() ? 'primary' : 'danger'"
>
<!-- 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-up" v-if="settings.advanced_search_visible"></i>
</b-button>
</b-input-group-append>
</b-input-group>
</div>
</div>
<b-collapse id="collapse_advanced_search" class="mt-2 shadow-sm" v-model="settings.advanced_search_visible">
<div class="card">
<div class="card-body p-4">
<div class="row">
<div class="col-md-3">
<a class="btn btn-primary btn-block text-uppercase"
:href="resolveDjangoUrl('new_recipe')">{{ $t('New_Recipe') }}</a>
<a class="btn btn-primary btn-block text-uppercase" :href="resolveDjangoUrl('new_recipe')">{{ $t("New_Recipe") }}</a>
</div>
<div class="col-md-3">
<a class="btn btn-primary btn-block text-uppercase"
:href="resolveDjangoUrl('data_import_url')">{{ $t('Import') }}</a>
<a class="btn btn-primary btn-block text-uppercase" :href="resolveDjangoUrl('data_import_url')">{{ $t("Import") }}</a>
</div>
<div class="col-md-3">
<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}"
@click="settings.search_internal = !settings.search_internal;refreshData()">
{{ $t('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 }"
@click="
settings.search_internal = !settings.search_internal
refreshData()
"
>
{{ $t("Internal") }}
</button>
</div>
<div class="col-md-3">
<button id="id_settings_button" class="btn btn-primary btn-block text-uppercase"><i
class="fas fa-cog fa-lg m-1"></i>
</button>
<button id="id_settings_button" class="btn btn-primary btn-block text-uppercase"><i class="fas fa-cog fa-lg m-1"></i></button>
</div>
</div>
</div>
<b-popover
target="id_settings_button"
triggers="click"
placement="bottom"
:title="$t('Settings')">
<b-popover target="id_settings_button" triggers="click" placement="bottom" :title="$t('Settings')">
<div>
<b-form-group
v-bind:label="$t('Recently_Viewed')"
label-for="popover-input-1"
label-cols="6"
class="mb-3">
<b-form-input
type="number"
v-model="settings.recently_viewed"
id="popover-input-1"
size="sm"
></b-form-input>
<b-form-group v-bind:label="$t('Recently_Viewed')" label-for="popover-input-1" label-cols="6" class="mb-3">
<b-form-input type="number" v-model="settings.recently_viewed" id="popover-input-1" size="sm"></b-form-input>
</b-form-group>
<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 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-bind:label="$t('Recipes_per_page')"
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-if="settings.show_meal_plan"
v-bind:label="$t('Meal_Plan_Days')"
label-for="popover-input-5"
label-cols="6"
class="mb-3">
<b-form-input
type="number"
v-model="settings.meal_plan_days"
id="popover-input-5"
size="sm"
></b-form-input>
class="mb-3"
>
<b-form-input type="number" v-model="settings.meal_plan_days" id="popover-input-5" size="sm"></b-form-input>
</b-form-group>
<b-form-group
v-bind:label="$t('Sort_by_new')"
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 v-bind:label="$t('Sort_by_new')" 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>
</div>
<div class="row" style="margin-top: 1vh">
<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 class="row" style="margin-top: 1vh">
<div class="col-12" style="text-align: right">
<b-button size="sm" variant="secondary" style="margin-right:8px"
@click="$root.$emit('bv::hide::popover')">{{ $t('Close') }}
<b-button size="sm" variant="secondary" style="margin-right: 8px" @click="$root.$emit('bv::hide::popover')"
>{{ $t("Close") }}
</b-button>
</div>
</div>
@ -150,17 +110,28 @@
<div class="row">
<div class="col-12">
<b-input-group class="mt-2">
<treeselect v-model="settings.search_keywords" :options="facets.Keywords" :flat="true"
searchNested multiple :placeholder="$t('Keywords')" :normalizer="normalizer"
<treeselect
v-model="settings.search_keywords"
:options="facets.Keywords"
:flat="true"
searchNested
multiple
:placeholder="$t('Keywords')"
:normalizer="normalizer"
@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-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)"
class="shadow-none" switch>
<span class="text-uppercase" v-if="settings.search_keywords_or">{{ $t('or') }}</span>
<span class="text-uppercase" v-else>{{ $t('and') }}</span>
class="shadow-none"
switch
>
<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-input-group-text>
</b-input-group-append>
@ -172,17 +143,28 @@
<div class="row">
<div class="col-12">
<b-input-group class="mt-2">
<treeselect v-model="settings.search_foods" :options="facets.Foods" :flat="true"
searchNested multiple :placeholder="$t('Ingredients')" :normalizer="normalizer"
<treeselect
v-model="settings.search_foods"
:options="facets.Foods"
:flat="true"
searchNested
multiple
:placeholder="$t('Ingredients')"
:normalizer="normalizer"
@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-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)"
class="shadow-none" switch>
<span class="text-uppercase" v-if="settings.search_foods_or">{{ $t('or') }}</span>
<span class="text-uppercase" v-else>{{ $t('and') }}</span>
class="shadow-none"
switch
>
<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-input-group-text>
</b-input-group-append>
@ -194,18 +176,27 @@
<div class="row">
<div class="col-12">
<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"
:model="Models.RECIPE_BOOK"
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-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)"
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>
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-append>
@ -217,103 +208,95 @@
<div class="row">
<div class="col-12">
<b-input-group class="mt-2">
<treeselect v-model="settings.search_ratings" :options="ratingOptions" :flat="true"
:placeholder="$t('Ratings')" :searchable="false"
<treeselect
v-model="settings.search_ratings"
:options="ratingOptions"
:flat="true"
:placeholder="$t('Ratings')"
:searchable="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-text style="width:85px">
</b-input-group-text>
<b-input-group-text style="width: 85px"> </b-input-group-text>
</b-input-group-append>
</b-input-group>
</div>
</div>
</div>
</div>
</b-collapse>
</div>
</div>
<div class="row">
<div class="col col-md-12 text-right" style="margin-top: 2vh">
<span class="text-muted">
{{ $t('Page') }} {{ settings.pagination_page }}/{{ Math.ceil(pagination_count/settings.page_count) }} <a href="#" @click="resetSearch"><i
class="fas fa-times-circle"></i> {{ $t('Reset') }}</a>
{{ $t("Page") }} {{ settings.pagination_page }}/{{ Math.ceil(pagination_count / settings.page_count) }}
<a href="#" @click="resetSearch"><i class="fas fa-times-circle"></i> {{ $t("Reset") }}</a>
</span>
</div>
</div>
<div class="row">
<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">
<recipe-card v-bind:key="`mp_${m.id}`" 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>
<recipe-card
v-bind:key="`mp_${m.id}`"
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>
<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>
<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>
</div>
</div>
</div>
<div class="row" style="margin-top: 2vh" v-if="!random_search">
<div class="col col-md-12">
<b-pagination pills
v-model="settings.pagination_page"
:total-rows="pagination_count"
:per-page="settings.page_count"
@change="pageChange"
align="center">
<b-pagination pills v-model="settings.pagination_page" :total-rows="pagination_count" :per-page="settings.page_count" @change="pageChange" align="center">
</b-pagination>
</div>
</div>
</div>
<div class="col-md-2 d-none d-md-block">
</div>
<div class="col-md-2 d-none d-md-block"></div>
</div>
</div>
</template>
<script>
import Vue from 'vue'
import {BootstrapVue} from 'bootstrap-vue'
import Vue from "vue"
import { BootstrapVue } from "bootstrap-vue"
import 'bootstrap-vue/dist/bootstrap-vue.css'
import moment from 'moment'
import _debounce from 'lodash/debounce'
import "bootstrap-vue/dist/bootstrap-vue.css"
import moment from "moment"
import _debounce from "lodash/debounce"
import VueCookies from 'vue-cookies'
import VueCookies from "vue-cookies"
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 GenericMultiselect from "@/components/GenericMultiselect";
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
import RecipeCard from "@/components/RecipeCard"
import GenericMultiselect from "@/components/GenericMultiselect"
import Treeselect from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
Vue.use(BootstrapVue)
let SETTINGS_COOKIE_NAME = 'search_settings'
let SETTINGS_COOKIE_NAME = "search_settings"
export default {
name: 'RecipeSearchView',
name: "RecipeSearchView",
mixins: [ResolveUrlMixin, ApiMixin],
components: {GenericMultiselect, RecipeCard, Treeselect},
components: { GenericMultiselect, RecipeCard, Treeselect },
data() {
return {
// this.Models and this.Actions inherited from ApiMixin
@ -324,7 +307,7 @@ export default {
settings_loaded: false,
settings: {
search_input: '',
search_input: "",
search_internal: false,
search_keywords: [],
search_foods: [],
@ -344,30 +327,30 @@ export default {
},
pagination_count: 0,
random_search: false
random_search: false,
debug: false,
}
},
computed: {
ratingOptions: function () {
return [
{'id': 5, 'label': '⭐⭐⭐⭐⭐' + ' (' + (this.facets.Ratings?.['5.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': 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('Unrated') + ' (' + (this.facets.Ratings?.['0.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: 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: 1, label: "⭐ " + this.$t("and_up") + " (" + (this.facets.Ratings?.["1.0"] ?? 0) + ")" },
{ id: -1, label: this.$t("Unrated") + " (" + (this.facets.Ratings?.["0.0"] ?? 0) + ")" },
]
},
searchFiltered: function () {
if (
this.settings?.search_input === ''
&& this.settings?.search_keywords?.length === 0
&& this.settings?.search_foods?.length === 0
&& this.settings?.search_books?.length === 0
&& this.settings?.pagination_page === 1
&& !this.random_search
&& this.settings?.search_ratings === undefined
this.settings?.search_input === "" &&
this.settings?.search_keywords?.length === 0 &&
this.settings?.search_foods?.length === 0 &&
this.settings?.search_books?.length === 0 &&
this.settings?.pagination_page === 1 &&
!this.random_search &&
this.settings?.search_ratings === undefined
) {
return false
} else {
@ -380,55 +363,56 @@ export default {
if (this.$cookies.isKey(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.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.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.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.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.facets.Books.push({ id: x, name: "loading..." })
}
this.loadMealPlan()
this.refreshData(false)
})
this.$i18n.locale = window.CUSTOM_LOCALE
this.debug = localStorage.getItem("DEBUG") || false
},
watch: {
settings: {
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()
},
'settings.meal_plan_days': function () {
"settings.meal_plan_days": function () {
this.loadMealPlan()
},
'settings.recently_viewed': function () {
"settings.recently_viewed": function () {
this.refreshData(false)
},
'settings.search_input': _debounce(function () {
"settings.search_input": _debounce(function () {
this.settings.pagination_page = 1
this.pagination_count = 0
this.refreshData(false)
}, 300),
'settings.page_count': _debounce(function () {
"settings.page_count": _debounce(function () {
this.refreshData(false)
}, 300),
},
@ -437,59 +421,59 @@ export default {
refreshData: function (random) {
this.random_search = random
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"];
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
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}}
params.options = { query: { last_viewed: this.settings.recently_viewed } }
}
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then(result => {
window.scrollTo(0, 0);
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {
window.scrollTo(0, 0)
this.pagination_count = result.data.count
this.facets = result.data.facets
if(this.facets?.cache_key) {
if (this.facets?.cache_key) {
this.getFacets(this.facets.cache_key)
}
this.recipes = this.removeDuplicates(result.data.results, recipe => recipe.id)
if (!this.searchFiltered){
this.recipes = this.removeDuplicates(result.data.results, (recipe) => recipe.id)
if (!this.searchFiltered) {
// if meal plans are being shown - filter out any meal plan recipes from the recipe list
let mealPlans = []
this.meal_plans.forEach(x => mealPlans.push(x.recipe.id))
this.recipes = this.recipes.filter(recipe => !mealPlans.includes(recipe.id))
this.meal_plans.forEach((x) => mealPlans.push(x.recipe.id))
this.recipes = this.recipes.filter((recipe) => !mealPlans.includes(recipe.id))
}
})
},
openRandom: function () {
this.refreshData(true)
},
removeDuplicates: function(data, key) {
return [
...new Map(data.map(item => [key(item), item])).values()
]
removeDuplicates: function (data, key) {
return [...new Map(data.map((item) => [key(item), item])).values()]
},
loadMealPlan: function () {
if (this.settings.show_meal_plan) {
let params = {
'options': {'query':{
'from_date': moment().format('YYYY-MM-DD'),
'to_date': moment().add(this.settings.meal_plan_days, 'days').format('YYYY-MM-DD')
}}
options: {
query: {
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
})
} else {
@ -501,7 +485,7 @@ export default {
this.refreshData(false)
},
resetSearch: function () {
this.settings.search_input = ''
this.settings.search_input = ""
this.settings.search_internal = false
this.settings.search_keywords = []
this.settings.search_foods = []
@ -515,20 +499,20 @@ export default {
this.refreshData(false)
},
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) {
let count = (node?.count ? ' (' + node.count + ')' : '')
let count = node?.count ? " (" + node.count + ")" : ""
return {
id: node.id,
label: node.name + count,
children: node.children,
isDefaultExpanded: node.isDefaultExpanded
isDefaultExpanded: node.isDefaultExpanded,
}
},
isRecentOrNew: function(x) {
let recent_recipe = [this.$t('Recently_Viewed'), "fas fa-eye"]
let new_recipe = [this.$t('New_Recipe'), "fas fa-splotch"]
isRecentOrNew: function (x) {
let recent_recipe = [this.$t("Recently_Viewed"), "fas fa-eye"]
let new_recipe = [this.$t("New_Recipe"), "fas fa-splotch"]
if (x.new) {
return new_recipe
} else if (this.facets.Recent.includes(x.id)) {
@ -537,18 +521,43 @@ export default {
return [undefined, undefined]
}
},
getFacets: function(hash) {
this.genericGetAPI('api_get_facets', {hash: hash}).then((response) => {
this.facets = {...this.facets, ...response.data.facets}
getFacets: function (hash) {
this.genericGetAPI("api_get_facets", { hash: hash }).then((response) => {
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>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
<style>
</style>
<style></style>

View File

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