From 5e36bd0c278dfc1a97e0d30b2bae83b7fbbcbfc4 Mon Sep 17 00:00:00 2001 From: Chris Scoggins Date: Thu, 20 Jan 2022 14:29:44 -0600 Subject: [PATCH] complex food filters --- cookbook/helper/recipe_search.py | 46 ++++++--- cookbook/views/api.py | 5 +- .../RecipeSearchView/RecipeSearchView.vue | 95 ++++++++++++------- vue/src/utils/models.js | 5 +- vue/src/utils/openapi/api.ts | 54 ++++++++--- 5 files changed, 138 insertions(+), 67 deletions(-) diff --git a/cookbook/helper/recipe_search.py b/cookbook/helper/recipe_search.py index 24e2faadf..d5e0bd692 100644 --- a/cookbook/helper/recipe_search.py +++ b/cookbook/helper/recipe_search.py @@ -38,7 +38,12 @@ class RecipeSearch(): 'and_not': self._params.get('keywords_and_not', None) } - self._foods = self._params.get('foods', None) + self._foods = { + 'or': self._params.get('foods_or', None), + 'and': self._params.get('foods_and', None), + 'or_not': self._params.get('foods_or_not', None), + 'and_not': self._params.get('foods_and_not', None) + } self._books = self._params.get('books', None) self._steps = self._params.get('steps', None) self._units = self._params.get('units', None) @@ -48,7 +53,6 @@ class RecipeSearch(): self._sort_order = self._params.get('sort_order', None) # TODO add save - self._foods_or = str2bool(self._params.get('foods_or', True)) self._books_or = str2bool(self._params.get('books_or', True)) self._internal = str2bool(self._params.get('internal', False)) @@ -94,7 +98,7 @@ class RecipeSearch(): # self._last_viewed() # self._last_cooked() self.keyword_filters(**self._keywords) - self.food_filters(foods=self._foods, operator=self._foods_or) + self.food_filters(**self._foods) self.book_filters(books=self._books, operator=self._books_or) self.rating_filter(rating=self._rating) self.internal_filter() @@ -213,19 +217,31 @@ class RecipeSearch(): if 'not' in kw_filter: self._queryset = self._queryset.exclude(id__in=recipes.values('id')) - def food_filters(self, foods=None, operator=True): - if not foods: + def food_filters(self, **kwargs): + if all([kwargs[x] is None for x in kwargs]): return - if not isinstance(foods, list): - foods = [foods] - if operator == True: - # TODO creating setting to include descendants of food a setting - self._queryset = self._queryset.filter(steps__ingredients__food__in=Food.include_descendants(Food.objects.filter(pk__in=foods))) - else: - # when performing an 'and' search returned recipes should include a parent OR any of its descedants - # AND other foods selected so filters are appended using steps__ingredients__food__id__in the list of foods and descendants - for fd in Food.objects.filter(pk__in=foods): - self._queryset = self._queryset.filter(steps__ingredients__food__in=list(fd.get_descendants_and_self())) + for fd_filter in kwargs: + if not kwargs[fd_filter]: + continue + if not isinstance(kwargs[fd_filter], list): + kwargs[fd_filter] = [kwargs[fd_filter]] + + foods = Food.objects.filter(pk__in=kwargs[fd_filter]) + if 'or' in fd_filter: + f = Q(steps__ingredients__food__in=Food.include_descendants(foods)) + if 'not' in fd_filter: + self._queryset = self._queryset.exclude(f) + else: + self._queryset = self._queryset.filter(f) + elif 'and' in fd_filter: + recipes = Recipe.objects.all() + for food in foods: + if 'not' in fd_filter: + recipes = recipes.filter(steps__ingredients__food__in=food.get_descendants_and_self()) + else: + self._queryset = self._queryset.filter(steps__ingredients__food__in=food.get_descendants_and_self()) + if 'not' in fd_filter: + self._queryset = self._queryset.exclude(id__in=recipes.values('id')) def unit_filters(self, units=None, operator=True): if operator != True: diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 9c9801246..36460e3e0 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -639,10 +639,13 @@ class RecipeViewSet(viewsets.ModelViewSet): QueryParam(name='keywords_or_not', description=_('Keyword IDs, repeat for multiple Exclude recipes with any of the keywords.'), qtype='int'), QueryParam(name='keywords_and_not', description=_('Keyword IDs, repeat for multiple Exclude recipes with all of the keywords.'), qtype='int'), QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'), qtype='int'), + QueryParam(name='foods_or', description=_('Food IDs, repeat for multiple Return recipes with any of the foods'), qtype='int'), + QueryParam(name='foods_and', description=_('Food IDs, repeat for multiple Return recipes with all of the foods.'), qtype='int'), + QueryParam(name='foods_or_not', description=_('Food IDs, repeat for multiple Exclude recipes with any of the foods.'), qtype='int'), + QueryParam(name='foods_and_not', description=_('Food IDs, repeat for multiple Exclude recipes with all of the foods.'), qtype='int'), QueryParam(name='units', description=_('ID of unit a recipe should have.'), qtype='int'), QueryParam(name='rating', description=_('Rating a recipe should have. [0 - 5]'), qtype='int'), QueryParam(name='books', description=_('ID of book a recipe should be in. For multiple repeat parameter.')), - QueryParam(name='foods_or', description=_('If recipe should have all (AND=''false'') or any (OR=''true'') of the provided foods.')), QueryParam(name='books_or', description=_('If recipe should be in all (AND=''false'') or any (OR=''true'') of the provided books.')), QueryParam(name='internal', description=_('If only internal recipes should be returned. [''true''/''false'']')), QueryParam(name='random', description=_('Returns the results in randomized order. [''true''/''false'']')), diff --git a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue index c609e9404..dc0103591 100644 --- a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue +++ b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue @@ -141,28 +141,28 @@ -
{{ $t("Keywords") }}
+
{{ $t("Keywords") }}
- + + /> - {{ $t("or") }} + {{ $t("or") }} {{ $t("and") }} - + {{ $t("not") }} @@ -211,12 +211,28 @@
+
{{ $t("Foods") }}
- + + + /> - - {{ $t("or") }} + + {{ $t("or") }} {{ $t("and") }} + + + + {{ $t("not") }} + + +
@@ -409,22 +432,25 @@ export default { { items: [], operator: true, not: false }, { items: [], operator: true, not: false }, ], - search_foods: [], + search_foods: [ + { items: [], operator: true, not: false }, + { items: [], operator: true, not: false }, + { items: [], operator: true, not: false }, + { items: [], operator: true, not: false }, + ], search_books: [], search_units: [], search_rating: undefined, search_rating_gte: true, - search_keywords_or: true, - search_foods_or: true, search_books_or: true, search_units_or: true, pagination_page: 1, expert_mode: false, keywords_fields: 1, - food_fields: 1, - book_fields: 1, + foods_fields: 1, + books_fields: 1, rating_fields: 1, - unit_fields: 1, + units_fields: 1, }, ui: { show_meal_plan: true, @@ -497,16 +523,16 @@ export default { return !this.expertMode ? 1 : this.search.keywords_fields }, foodFields: function () { - return !this.expertMode ? 1 : this.search.food_fields + return !this.expertMode ? 1 : this.search.foods_fields }, bookFields: function () { - return !this.expertMode ? 1 : this.search.book_fields + return !this.expertMode ? 1 : this.search.books_fields }, ratingFields: function () { return !this.expertMode ? 1 : this.search.rating_fields }, unitFields: function () { - return !this.expertMode ? 1 : this.search.unit_fields + return !this.expertMode ? 1 : this.search.units_fields }, }, mounted() { @@ -544,7 +570,7 @@ export default { } this.facets.Foods = [] - for (let x of this.search.search_foods) { + for (let x of this.search.search_foods.map((x) => x.items).flat()) { this.facets.Foods.push({ id: x, name: "loading..." }) } @@ -663,7 +689,9 @@ export default { this.search.search_keywords = this.search.search_keywords.map((x) => { return { ...x, items: [] } }) - this.search.search_foods = [] + this.search.search_foods = this.search.search_foods.map((x) => { + return { ...x, items: [] } + }) this.search.search_books = [] this.search.search_units = [] this.search.search_rating = undefined @@ -735,15 +763,12 @@ export default { this.addFields("keywords") let params = { ...this.addFields("keywords"), + ...this.addFields("foods"), query: this.search.search_input, - foods: this.search.search_foods.map(function (A) { - return A?.["id"] ?? A - }), rating: rating, books: this.search.search_books.map(function (A) { return A["id"] }), - foodsOr: this.search.search_foods_or, booksOr: this.search.search_books_or, internal: this.search.search_internal, random: this.random_search, @@ -760,7 +785,7 @@ export default { searchFiltered: function (ignore_string = false) { let filtered = this.search?.search_keywords[0].items?.length === 0 && - this.search?.search_foods?.length === 0 && + this.search?.search_foods[0].items?.length === 0 && this.search?.search_books?.length === 0 && // this.settings?.pagination_page === 1 && !this.random_search && diff --git a/vue/src/utils/models.js b/vue/src/utils/models.js index c4845aed9..2639c5c33 100644 --- a/vue/src/utils/models.js +++ b/vue/src/utils/models.js @@ -441,10 +441,13 @@ export class Models { "keywords_or_not", "keywords_and_not", "foods", + "foods_or", + "foods_and", + "foods_or_not", + "foods_and_not", "units", "rating", "books", - "foodsOr", "booksOr", "internal", "random", diff --git a/vue/src/utils/openapi/api.ts b/vue/src/utils/openapi/api.ts index 0d78c8301..84f411686 100644 --- a/vue/src/utils/openapi/api.ts +++ b/vue/src/utils/openapi/api.ts @@ -5288,10 +5288,13 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) * @param {number} [keywordsOrNot] Keyword IDs, repeat for multiple Exclude recipes with any of the keywords. * @param {number} [keywordsAndNot] Keyword IDs, repeat for multiple Exclude recipes with all of the keywords. * @param {number} [foods] ID of food a recipe should have. For multiple repeat parameter. + * @param {number} [foodsOr] Food IDs, repeat for multiple Return recipes with any of the foods + * @param {number} [foodsAnd] Food IDs, repeat for multiple Return recipes with all of the foods. + * @param {number} [foodsOrNot] Food IDs, repeat for multiple Exclude recipes with any of the foods. + * @param {number} [foodsAndNot] Food IDs, repeat for multiple Exclude recipes with all of the foods. * @param {number} [units] ID of unit a recipe should have. * @param {number} [rating] Rating a recipe should have. [0 - 5] * @param {string} [books] ID of book a recipe should be in. For multiple repeat parameter. - * @param {string} [foodsOr] If recipe should have all (AND=false) or any (OR=<b>true</b>) of the provided foods. * @param {string} [booksOr] If recipe should be in all (AND=false) or any (OR=<b>true</b>) of the provided books. * @param {string} [internal] If only internal recipes should be returned. [true/<b>false</b>] * @param {string} [random] Returns the results in randomized order. [true/<b>false</b>] @@ -5301,7 +5304,7 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listRecipes: async (query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, units?: number, rating?: number, books?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options: any = {}): Promise => { + listRecipes: async (query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options: any = {}): Promise => { const localVarPath = `/api/recipe/`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -5342,6 +5345,22 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) localVarQueryParameter['foods'] = foods; } + if (foodsOr !== undefined) { + localVarQueryParameter['foods_or'] = foodsOr; + } + + if (foodsAnd !== undefined) { + localVarQueryParameter['foods_and'] = foodsAnd; + } + + if (foodsOrNot !== undefined) { + localVarQueryParameter['foods_or_not'] = foodsOrNot; + } + + if (foodsAndNot !== undefined) { + localVarQueryParameter['foods_and_not'] = foodsAndNot; + } + if (units !== undefined) { localVarQueryParameter['units'] = units; } @@ -5354,10 +5373,6 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) localVarQueryParameter['books'] = books; } - if (foodsOr !== undefined) { - localVarQueryParameter['foods_or'] = foodsOr; - } - if (booksOr !== undefined) { localVarQueryParameter['books_or'] = booksOr; } @@ -9693,10 +9708,13 @@ export const ApiApiFp = function(configuration?: Configuration) { * @param {number} [keywordsOrNot] Keyword IDs, repeat for multiple Exclude recipes with any of the keywords. * @param {number} [keywordsAndNot] Keyword IDs, repeat for multiple Exclude recipes with all of the keywords. * @param {number} [foods] ID of food a recipe should have. For multiple repeat parameter. + * @param {number} [foodsOr] Food IDs, repeat for multiple Return recipes with any of the foods + * @param {number} [foodsAnd] Food IDs, repeat for multiple Return recipes with all of the foods. + * @param {number} [foodsOrNot] Food IDs, repeat for multiple Exclude recipes with any of the foods. + * @param {number} [foodsAndNot] Food IDs, repeat for multiple Exclude recipes with all of the foods. * @param {number} [units] ID of unit a recipe should have. * @param {number} [rating] Rating a recipe should have. [0 - 5] * @param {string} [books] ID of book a recipe should be in. For multiple repeat parameter. - * @param {string} [foodsOr] If recipe should have all (AND=false) or any (OR=<b>true</b>) of the provided foods. * @param {string} [booksOr] If recipe should be in all (AND=false) or any (OR=<b>true</b>) of the provided books. * @param {string} [internal] If only internal recipes should be returned. [true/<b>false</b>] * @param {string} [random] Returns the results in randomized order. [true/<b>false</b>] @@ -9706,8 +9724,8 @@ export const ApiApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, units?: number, rating?: number, books?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, units, rating, books, foodsOr, booksOr, internal, random, _new, page, pageSize, options); + async listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, foodsOr, foodsAnd, foodsOrNot, foodsAndNot, units, rating, books, booksOr, internal, random, _new, page, pageSize, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -11381,10 +11399,13 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: * @param {number} [keywordsOrNot] Keyword IDs, repeat for multiple Exclude recipes with any of the keywords. * @param {number} [keywordsAndNot] Keyword IDs, repeat for multiple Exclude recipes with all of the keywords. * @param {number} [foods] ID of food a recipe should have. For multiple repeat parameter. + * @param {number} [foodsOr] Food IDs, repeat for multiple Return recipes with any of the foods + * @param {number} [foodsAnd] Food IDs, repeat for multiple Return recipes with all of the foods. + * @param {number} [foodsOrNot] Food IDs, repeat for multiple Exclude recipes with any of the foods. + * @param {number} [foodsAndNot] Food IDs, repeat for multiple Exclude recipes with all of the foods. * @param {number} [units] ID of unit a recipe should have. * @param {number} [rating] Rating a recipe should have. [0 - 5] * @param {string} [books] ID of book a recipe should be in. For multiple repeat parameter. - * @param {string} [foodsOr] If recipe should have all (AND=false) or any (OR=<b>true</b>) of the provided foods. * @param {string} [booksOr] If recipe should be in all (AND=false) or any (OR=<b>true</b>) of the provided books. * @param {string} [internal] If only internal recipes should be returned. [true/<b>false</b>] * @param {string} [random] Returns the results in randomized order. [true/<b>false</b>] @@ -11394,8 +11415,8 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, units?: number, rating?: number, books?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): AxiosPromise { - return localVarFp.listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, units, rating, books, foodsOr, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(axios, basePath)); + listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): AxiosPromise { + return localVarFp.listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, foodsOr, foodsAnd, foodsOrNot, foodsAndNot, units, rating, books, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(axios, basePath)); }, /** * @@ -13093,10 +13114,13 @@ export class ApiApi extends BaseAPI { * @param {number} [keywordsOrNot] Keyword IDs, repeat for multiple Exclude recipes with any of the keywords. * @param {number} [keywordsAndNot] Keyword IDs, repeat for multiple Exclude recipes with all of the keywords. * @param {number} [foods] ID of food a recipe should have. For multiple repeat parameter. + * @param {number} [foodsOr] Food IDs, repeat for multiple Return recipes with any of the foods + * @param {number} [foodsAnd] Food IDs, repeat for multiple Return recipes with all of the foods. + * @param {number} [foodsOrNot] Food IDs, repeat for multiple Exclude recipes with any of the foods. + * @param {number} [foodsAndNot] Food IDs, repeat for multiple Exclude recipes with all of the foods. * @param {number} [units] ID of unit a recipe should have. * @param {number} [rating] Rating a recipe should have. [0 - 5] * @param {string} [books] ID of book a recipe should be in. For multiple repeat parameter. - * @param {string} [foodsOr] If recipe should have all (AND=false) or any (OR=<b>true</b>) of the provided foods. * @param {string} [booksOr] If recipe should be in all (AND=false) or any (OR=<b>true</b>) of the provided books. * @param {string} [internal] If only internal recipes should be returned. [true/<b>false</b>] * @param {string} [random] Returns the results in randomized order. [true/<b>false</b>] @@ -13107,8 +13131,8 @@ export class ApiApi extends BaseAPI { * @throws {RequiredError} * @memberof ApiApi */ - public listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, units?: number, rating?: number, books?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any) { - return ApiApiFp(this.configuration).listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, units, rating, books, foodsOr, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(this.axios, this.basePath)); + public listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any) { + return ApiApiFp(this.configuration).listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, foodsOr, foodsAnd, foodsOrNot, foodsAndNot, units, rating, books, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(this.axios, this.basePath)); } /**