v2 search filtering

This commit is contained in:
vabene1111 2021-04-17 21:28:29 +02:00
parent d1d65d878c
commit 880db58d38
10 changed files with 161 additions and 49 deletions

View File

@ -0,0 +1,56 @@
from functools import reduce
from django.contrib.postgres.search import TrigramSimilarity
from django.db.models import Q
from recipes import settings
def search_recipes(queryset, params):
search_string = params.get('query', '')
search_keywords = params.getlist('keywords', [])
search_foods = params.getlist('foods', [])
search_books = params.getlist('books', [])
search_keywords_or = params.get('keywords_or', True)
search_foods_or = params.get('foods_or', True)
search_books_or = params.get('books_or', True)
search_internal = params.get('internal', None)
search_limit = params.get('limit', None)
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
queryset = queryset.annotate(similarity=TrigramSimilarity('name', search_string), ).filter(Q(similarity__gt=0.1) | Q(name__unaccent__icontains=search_string)).order_by('-similarity')
else:
queryset = queryset.filter(name__icontains=search_string)
if len(search_keywords) > 0:
if search_keywords_or == 'true':
queryset = queryset.filter(keywords__id__in=search_keywords)
else:
for k in search_keywords:
queryset = queryset.filter(keywords__id=k)
if len(search_foods) > 0:
if search_foods_or == 'true':
queryset = queryset.filter(keywords__id__in=search_foods)
else:
for k in search_foods:
queryset = queryset.filter(keywords__id=k)
if len(search_books) > 0:
if search_books_or == 'true':
queryset = queryset.filter(keywords__id__in=search_books)
else:
for k in search_books:
queryset = queryset.filter(keywords__id=k)
queryset = queryset.distinct()
if search_internal == 'true':
queryset = queryset.filter(internal=True)
if search_limit:
queryset = queryset[:int(search_limit)]
return queryset

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -28,6 +28,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
CustomIsOwner, CustomIsShare,
CustomIsShared, CustomIsUser,
group_required, share_link_valid)
from cookbook.helper.recipe_search import search_recipes
from cookbook.helper.recipe_url_import import get_from_html, get_from_scraper, find_recipe_json
from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
MealType, Recipe, RecipeBook, ShoppingList,
@ -269,7 +270,7 @@ class StepViewSet(viewsets.ModelViewSet):
return self.queryset.filter(recipe__space=self.request.space)
class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin):
class RecipeViewSet(viewsets.ModelViewSet):
"""
list:
optional parameters
@ -288,11 +289,7 @@ class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin):
if not (share and self.detail):
self.queryset = self.queryset.filter(space=self.request.space)
internal = self.request.query_params.get('internal', None)
if internal:
self.queryset = self.queryset.filter(internal=True)
return super().get_queryset()
return search_recipes(self.queryset, self.request.GET)
# TODO write extensive tests for permissions

View File

@ -10,41 +10,67 @@
<div class="row">
<div class="col col-md-12">
<b-input class="form-control" v-model="search_input" @keyup="refreshData"
v-bind:placeholder="$t('Search')"></b-input>
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<a class="card-link" :href="resolveDjangoUrl('new_recipe')">new Recipe</a>
</div>
<div class="col-md-4">
<a class="card-link" :href="resolveDjangoUrl('data_import_url')">URL Import</a>
</div>
<div class="col-md-4">
<a class="card-link" href="#">Rest Search</a>
</div>
</div>
<div class="row">
<div class="col-md-12">
<generic-multiselect @change="genericSelectChanged" parent_variable="search_keywords"
search_function="listKeywords" label="label"></generic-multiselect>
<generic-multiselect @change="genericSelectChanged" parent_variable="search_foods"
search_function="listFoods" label="name"></generic-multiselect>
<generic-multiselect @change="genericSelectChanged" parent_variable="search_books"
search_function="listRecipeBooks" label="name"></generic-multiselect>
<b-input-group class="mt-3">
<b-input class="form-control" v-model="search_input" @keyup="refreshData"
v-bind:placeholder="$t('Search')"></b-input>
<b-input-group-append>
<b-button v-b-toggle.collapse_advanced_search variant="primary" class="shadow-none"><i
class="fas fa-caret-down" v-if="!advanced_search_visible"></i><i class="fas fa-caret-up"
v-if="advanced_search_visible"></i>
</b-button>
</b-input-group-append>
</b-input-group>
<b-collapse id="collapse_advanced_search" class="mt-2" v-model="advanced_search_visible">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<a class="card-link" :href="resolveDjangoUrl('new_recipe')">new Recipe</a>
</div>
<div class="col-md-4">
<a class="card-link" :href="resolveDjangoUrl('data_import_url')">URL Import</a>
</div>
<div class="col-md-4">
<a class="card-link" href="#">Rest Search</a>
</div>
<div class="col-md-4">
<input type="checkbox" v-model="search_internal" @change="refreshData"> Internal only
</div>
</div>
<div class="row">
<div class="col-md-12">
<generic-multiselect @change="genericSelectChanged" parent_variable="search_keywords"
style="margin-top: 1vh"
search_function="listKeywords" label="label"
v-bind:placeholder="$t('Keywords')"></generic-multiselect>
<input type="checkbox" v-model="search_keywords_or" @change="genericSelectChanged">
<generic-multiselect @change="genericSelectChanged" parent_variable="search_foods"
style="margin-top: 1vh"
search_function="listFoods" label="name"
v-bind:placeholder="$t('Ingredients')"></generic-multiselect>
<input type="checkbox" v-model="search_foods_or" @change="genericSelectChanged">
<generic-multiselect @change="genericSelectChanged" parent_variable="search_books"
style="margin-top: 1vh"
search_function="listRecipeBooks" label="name"
v-bind:placeholder="$t('Books')"></generic-multiselect>
<input type="checkbox" v-model="search_books_or" @change="genericSelectChanged">
</div>
</div>
</div>
</div>
</div>
</b-collapse>
</div>
</div>
<div class="row">
</div>
<div class="row" style="margin-top: 2vh">
<div class="col col-md-12">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;">
@ -87,11 +113,17 @@ export default {
return {
recipes: [],
search_input: '',
search_internal: false,
search_keywords: [],
search_foods: [],
search_books: [],
search_keywords_or: true,
search_foods_or: true,
search_books_or: true,
advanced_search_visible: true,
}
},
@ -102,12 +134,33 @@ export default {
refreshData: function () {
let apiClient = new ApiApiFactory()
apiClient.listRecipes({query: {query: this.search_input, limit: 20}}).then(result => {
apiClient.listRecipes({
query: {
query: this.search_input,
keywords: this.search_keywords.map(function (A) {
return A["id"];
}),
foods: this.search_foods.map(function (A) {
return A["id"];
}),
books: this.search_books.map(function (A) {
return A["id"];
}),
keywords_or: this.search_keywords_or,
foods_or: this.search_foods_or,
books_or: this.search_books_or,
internal: this.search_internal,
limit: 20,
}
}).then(result => {
this.recipes = result.data
})
},
genericSelectChanged: function (obj) {
this[obj.var] = obj.val
this.refreshData()
}
}
}

View File

@ -2,35 +2,40 @@
<b-card no-body>
<b-card-img-lazy style="height: 15vh; object-fit: cover" :src=recipe_image v-bind:alt="$t('Recipe_Image')"
top></b-card-img-lazy>
<a :href="resolveDjangoUrl('view_recipe', recipe.id)">
<b-card-img-lazy style="height: 15vh; object-fit: cover" :src=recipe_image v-bind:alt="$t('Recipe_Image')"
top></b-card-img-lazy>
<div class="card-img-overlay h-100 d-flex flex-column justify-content-right"
<div class="card-img-overlay h-100 d-flex flex-column justify-content-right"
style="float:right; text-align: right; padding-top: 10px; padding-right: 5px">
<recipe-context-menu :recipe="recipe" style="float:right"></recipe-context-menu>
</div>
<b-card-body :title=recipe.name title-tag="h5">
</a>
<b-card-body>
<h5><a :href="resolveDjangoUrl('view_recipe', recipe.id)">{{ recipe.name }}</a></h5>
<b-card-text style="text-overflow: ellipsis">
{{ recipe.description }}
<keywords :recipe="recipe" style="margin-top: 4px"></keywords>
</b-card-text>
</b-card-body>
</b-card>
</template>
<script>
import RecipeContextMenu from "@/components/RecipeContextMenu";
import Keywords from "@/components/Keywords";
import {ResolveUrlMixin} from "@/utils/utils";
export default {
name: "RecipeCard",
mixins: [
ResolveUrlMixin,
],
components: {Keywords, RecipeContextMenu},
props: {
recipe: Object,

View File

@ -16,7 +16,8 @@
"View_Recipes": "View Recipes",
"Log_Cooking": "Log Cooking",
"Keywords": "Keywords",
"Books": "Books",
"Proteins": "Proteins",
"Fats": "Fats",
"Carbohydrates": "Carbohydrates",