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

View File

@ -10,41 +10,67 @@
<div class="row"> <div class="row">
<div class="col col-md-12"> <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"> <b-input-group class="mt-3">
<generic-multiselect @change="genericSelectChanged" parent_variable="search_keywords" <b-input class="form-control" v-model="search_input" @keyup="refreshData"
search_function="listKeywords" label="label"></generic-multiselect> v-bind:placeholder="$t('Search')"></b-input>
<generic-multiselect @change="genericSelectChanged" parent_variable="search_foods" <b-input-group-append>
search_function="listFoods" label="name"></generic-multiselect> <b-button v-b-toggle.collapse_advanced_search variant="primary" class="shadow-none"><i
<generic-multiselect @change="genericSelectChanged" parent_variable="search_books" class="fas fa-caret-down" v-if="!advanced_search_visible"></i><i class="fas fa-caret-up"
search_function="listRecipeBooks" label="name"></generic-multiselect> 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>
</div> </div>
</div> </b-collapse>
</div> </div>
</div> </div>
<div class="row">
</div>
<div class="row" style="margin-top: 2vh"> <div class="row" style="margin-top: 2vh">
<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: 1rem;"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;">
@ -87,11 +113,17 @@ export default {
return { return {
recipes: [], recipes: [],
search_input: '', search_input: '',
search_internal: false,
search_keywords: [], search_keywords: [],
search_foods: [], search_foods: [],
search_books: [], 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 () { refreshData: function () {
let apiClient = new ApiApiFactory() 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 this.recipes = result.data
}) })
}, },
genericSelectChanged: function (obj) { genericSelectChanged: function (obj) {
this[obj.var] = obj.val this[obj.var] = obj.val
this.refreshData()
} }
} }
} }

View File

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

View File

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