v2 search filtering
This commit is contained in:
parent
d1d65d878c
commit
880db58d38
56
cookbook/helper/recipe_search.py
Normal file
56
cookbook/helper/recipe_search.py
Normal 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
@ -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
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -16,7 +16,8 @@
|
||||
"View_Recipes": "View Recipes",
|
||||
"Log_Cooking": "Log Cooking",
|
||||
|
||||
|
||||
"Keywords": "Keywords",
|
||||
"Books": "Books",
|
||||
"Proteins": "Proteins",
|
||||
"Fats": "Fats",
|
||||
"Carbohydrates": "Carbohydrates",
|
||||
|
Loading…
Reference in New Issue
Block a user