diff --git a/cookbook/helper/recipe_search.py b/cookbook/helper/recipe_search.py index 1846f2e8..24e2faad 100644 --- a/cookbook/helper/recipe_search.py +++ b/cookbook/helper/recipe_search.py @@ -31,7 +31,13 @@ class RecipeSearch(): self._search_prefs = SearchPreference() self._string = params.get('query').strip() if params.get('query', None) else None self._rating = self._params.get('rating', None) - self._keywords = self._params.get('keywords', None) + self._keywords = { + 'or': self._params.get('keywords_or', None), + 'and': self._params.get('keywords_and', None), + 'or_not': self._params.get('keywords_or_not', None), + 'and_not': self._params.get('keywords_and_not', None) + } + self._foods = self._params.get('foods', None) self._books = self._params.get('books', None) self._steps = self._params.get('steps', None) @@ -42,7 +48,6 @@ class RecipeSearch(): self._sort_order = self._params.get('sort_order', None) # TODO add save - self._keywords_or = str2bool(self._params.get('keywords_or', True)) self._foods_or = str2bool(self._params.get('foods_or', True)) self._books_or = str2bool(self._params.get('books_or', True)) @@ -88,7 +93,7 @@ class RecipeSearch(): self._new_recipes() # self._last_viewed() # self._last_cooked() - self.keyword_filters(keywords=self._keywords, operator=self._keywords_or) + self.keyword_filters(**self._keywords) self.food_filters(foods=self._foods, operator=self._foods_or) self.book_filters(books=self._books, operator=self._books_or) self.rating_filter(rating=self._rating) @@ -182,19 +187,31 @@ class RecipeSearch(): ).values('recipe').annotate(count=Count('pk', distinct=True)).values('count') self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), 0)) - def keyword_filters(self, keywords=None, operator=True): - if not keywords: + def keyword_filters(self, **kwargs): + if all([kwargs[x] is None for x in kwargs]): return - if not isinstance(keywords, list): - keywords = [keywords] - if operator == True: - # TODO creating setting to include descendants of keywords a setting - self._queryset = self._queryset.filter(keywords__in=Keyword.include_descendants(Keyword.objects.filter(pk__in=keywords))) - else: - # when performing an 'and' search returned recipes should include a parent OR any of its descedants - # AND other keywords selected so filters are appended using keyword__id__in the list of keywords and descendants - for kw in Keyword.objects.filter(pk__in=keywords): - self._queryset = self._queryset.filter(keywords__in=list(kw.get_descendants_and_self())) + for kw_filter in kwargs: + if not kwargs[kw_filter]: + continue + if not isinstance(kwargs[kw_filter], list): + kwargs[kw_filter] = [kwargs[kw_filter]] + + keywords = Keyword.objects.filter(pk__in=kwargs[kw_filter]) + if 'or' in kw_filter: + f = Q(keywords__in=Keyword.include_descendants(keywords)) + if 'not' in kw_filter: + self._queryset = self._queryset.exclude(f) + else: + self._queryset = self._queryset.filter(f) + elif 'and' in kw_filter: + recipes = Recipe.objects.all() + for kw in keywords: + if 'not' in kw_filter: + recipes = recipes.filter(keywords__in=kw.get_descendants_and_self()) + else: + self._queryset = self._queryset.filter(keywords__in=kw.get_descendants_and_self()) + 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: @@ -229,7 +246,7 @@ class RecipeSearch(): if rating == 0: self._queryset = self._queryset.filter(rating=0) elif lessthan: - self._queryset = self._queryset.filter(rating__lte=int(rating[1:])) + self._queryset = self._queryset.filter(rating__lte=int(rating[1:])).exclude(rating=0) else: self._queryset = self._queryset.filter(rating__gte=int(rating)) diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 878c7118..9c980124 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -630,15 +630,18 @@ class RecipeViewSet(viewsets.ModelViewSet): # TODO split read and write permission for meal plan guest permission_classes = [CustomIsShare | CustomIsGuest] pagination_class = RecipePagination - # TODO the boolean params below (keywords_or through new) should be updated to boolean types with front end refactored accordingly + query_params = [ QueryParam(name='query', description=_('Query string matched (fuzzy) against recipe name. In the future also fulltext search.')), - QueryParam(name='keywords', description=_('ID of keyword a recipe should have. For multiple repeat parameter.'), qtype='int'), + QueryParam(name='keywords', description=_('ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'), qtype='int'), + QueryParam(name='keywords_or', description=_('Keyword IDs, repeat for multiple Return recipes with any of the keywords'), qtype='int'), + QueryParam(name='keywords_and', description=_('Keyword IDs, repeat for multiple Return recipes with all of the keywords.'), qtype='int'), + 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='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='keywords_or', description=_('If recipe should have all (AND=''false'') or any (OR=''true'') of the provided keywords.')), 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'']')), diff --git a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue index b95e79e6..c609e940 100644 --- a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue +++ b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue @@ -141,9 +141,25 @@ +
{{ $t("Keywords") }}
- + + - - {{ $t("or") }} + + {{ $t("or") }} {{ $t("and") }} + + + + {{ $t("not") }} + + +
@@ -199,8 +229,8 @@ style="flex-grow: 1; flex-shrink: 1; flex-basis: 0" /> - + {{ $t("or") }} {{ $t("and") }} @@ -235,7 +265,7 @@ > - + {{ $t("or") }} {{ $t("and") }} @@ -250,7 +280,7 @@
+ - + + + >= + <= + +
@@ -279,7 +317,7 @@ > - + {{ $t("or") }} {{ $t("and") }} @@ -339,8 +377,8 @@ import { ApiMixin, ResolveUrlMixin } from "@/utils/utils" import LoadingSpinner from "@/components/LoadingSpinner" // TODO: is this deprecated? import RecipeCard from "@/components/RecipeCard" import GenericMultiselect from "@/components/GenericMultiselect" -import { Treeselect, LOAD_CHILDREN_OPTIONS } from "@riophae/vue-treeselect" //TODO: delete -import "@riophae/vue-treeselect/dist/vue-treeselect.css" //TODO: delete +import { Treeselect, LOAD_CHILDREN_OPTIONS } from "@riophae/vue-treeselect" +import "@riophae/vue-treeselect/dist/vue-treeselect.css" import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher" Vue.use(VueCookies) @@ -366,15 +404,15 @@ export default { search_input: "", search_internal: false, search_keywords: [ - { items: [], operator: true }, - { items: [], operator: true }, - { items: [], operator: true }, - { items: [], operator: true }, + { items: [], operator: true, not: false }, + { items: [], operator: true, not: false }, + { items: [], operator: true, not: false }, + { items: [], operator: true, not: false }, ], search_foods: [], search_books: [], search_units: [], - search_ratings: undefined, + search_rating: undefined, search_rating_gte: true, search_keywords_or: true, search_foods_or: true, @@ -382,7 +420,11 @@ export default { search_units_or: true, pagination_page: 1, expert_mode: false, - keyword_fields: 1, + keywords_fields: 1, + food_fields: 1, + book_fields: 1, + rating_fields: 1, + unit_fields: 1, }, ui: { show_meal_plan: true, @@ -410,6 +452,7 @@ export default { computed: { ratingOptions: function () { let ratingCount = undefined + let label = undefined if (Object.keys(this.facets?.Ratings ?? {}).length === 0) { ratingCount = (x) => { return "" @@ -419,24 +462,52 @@ export default { return ` (${x})` } } - let label = "" + if (this.search.search_rating_gte) { - label = this.$t("and_up") + label = (x) => { + if (x == 5) { + return "" + } else { + return this.$t("and_up") + } + } } else { - label = this.$t("and_down") + label = (x) => { + if (x == 1) { + return "" + } else { + return this.$t("and_down") + } + } } + return [ - { id: 5, label: "⭐⭐⭐⭐⭐" + ratingCount(this.facets.Ratings?.["5.0"] ?? 0) }, - { id: 4, label: "⭐⭐⭐⭐ " + this.$t("and_up") + ratingCount(this.facets.Ratings?.["4.0"] ?? 0) }, - { id: 3, label: "⭐⭐⭐ " + this.$t("and_up") + ratingCount(this.facets.Ratings?.["3.0"] ?? 0) }, - { id: 2, label: "⭐⭐ " + this.$t("and_up") + ratingCount(this.facets.Ratings?.["2.0"] ?? 0) }, - { id: 1, label: "⭐ " + this.$t("and_up") + ratingCount(this.facets.Ratings?.["1.0"] ?? 0) }, + { id: 5, label: "⭐⭐⭐⭐⭐" + label(5) + ratingCount(this.facets.Ratings?.["5.0"] ?? 0) }, + { id: 4, label: "⭐⭐⭐⭐ " + label() + ratingCount(this.facets.Ratings?.["4.0"] ?? 0) }, + { id: 3, label: "⭐⭐⭐ " + label() + ratingCount(this.facets.Ratings?.["3.0"] ?? 0) }, + { id: 2, label: "⭐⭐ " + label() + ratingCount(this.facets.Ratings?.["2.0"] ?? 0) }, + { id: 1, label: "⭐ " + label(1) + ratingCount(this.facets.Ratings?.["1.0"] ?? 0) }, { id: 0, label: this.$t("Unrated") + ratingCount(this.facets.Ratings?.["0.0"] ?? 0) }, ] }, expertMode: function () { return this.ui.enable_expert && this.search.expert_mode }, + keywordFields: function () { + return !this.expertMode ? 1 : this.search.keywords_fields + }, + foodFields: function () { + return !this.expertMode ? 1 : this.search.food_fields + }, + bookFields: function () { + return !this.expertMode ? 1 : this.search.book_fields + }, + ratingFields: function () { + return !this.expertMode ? 1 : this.search.rating_fields + }, + unitFields: function () { + return !this.expertMode ? 1 : this.search.unit_fields + }, }, mounted() { this.$nextTick(function () { @@ -478,7 +549,7 @@ export default { } this.facets.Keywords = [] - for (let x of this.search.search_keywords) { + for (let x of this.search.search_keywords.map((x) => x.items).flat()) { this.facets.Keywords.push({ id: x, name: "loading..." }) } @@ -532,37 +603,7 @@ export default { methods: { // this.genericAPI inherited from ApiMixin refreshData: function (random) { - console.log(this.search.search_keywords) - this.random_search = random - let params = { - query: this.search.search_input, - keywords: this.search.search_keywords[0].items.map(function (A) { - return A?.["id"] ?? A - }), - foods: this.search.search_foods.map(function (A) { - return A?.["id"] ?? A - }), - rating: this.search.search_ratings, - units: this.search.search_units.map(function (A) { - return A["id"] - }), - books: this.search.search_books.map(function (A) { - return A["id"] - }), - keywordsOr: this.search.search_keywords_or, - foodsOr: this.search.search_foods_or, - booksOr: this.search.search_books_or, - unitsOr: this.search.search_units_or, - internal: this.search.search_internal, - random: this.random_search, - _new: this.ui.sort_by_new, - page: this.search.pagination_page, - pageSize: this.search.page_size, - } - if (!this.searchFiltered) { - params.options = { query: { last_viewed: this.ui.recently_viewed } } - } - // console.log(params, this.search.search_keywords[0], this.search.search_keywords[0].items) + let params = this.buildParams(random) this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params) .then((result) => { window.scrollTo(0, 0) @@ -619,11 +660,13 @@ export default { resetSearch: function () { this.search.search_input = "" this.search.search_internal = false - this.search.search_keywords[0].items = [] + this.search.search_keywords = this.search.search_keywords.map((x) => { + return { ...x, items: [] } + }) this.search.search_foods = [] this.search.search_books = [] this.search.search_units = [] - this.search.search_ratings = undefined + this.search.search_rating = undefined this.search.pagination_page = 1 this.refreshData(false) }, @@ -682,16 +725,24 @@ export default { } } }, - buildParams: function () { + buildParams: function (random) { + this.random_search = random + let rating = this.search.search_rating + if (rating !== undefined && !this.search.search_rating_gte) { + rating = rating * -1 + } + // TODO check expertmode + this.addFields("keywords") let params = { + ...this.addFields("keywords"), query: this.search.search_input, - keywords: this.search.search_keywords[0].items, - foods: this.search.search_foods, - rating: this.search.search_ratings, + 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"] }), - keywordsOr: this.search.search_keywords_or, foodsOr: this.search.search_foods_or, booksOr: this.search.search_books_or, internal: this.search.search_internal, @@ -703,6 +754,8 @@ export default { if (!this.searchFiltered()) { params.options = { query: { last_viewed: this.ui.recently_viewed } } } + console.log(params) + return params }, searchFiltered: function (ignore_string = false) { let filtered = @@ -711,7 +764,7 @@ export default { this.search?.search_books?.length === 0 && // this.settings?.pagination_page === 1 && !this.random_search && - this.search?.search_ratings === undefined + this.search?.search_rating === undefined if (ignore_string) { return !filtered @@ -719,10 +772,57 @@ export default { return !filtered && this.search?.search_input !== "" } }, + addFields(field) { + let fieldlist = this.search[`search_${field}`].slice(0, this.search[`${field}_fields`]) + return { + [`${field}_or`]: fieldlist + .filter((x) => x.operator == true && x.not == false) + .map((x) => x.items) + .flat() + .map((x) => x?.id ?? x), + [`${field}_and`]: fieldlist + .filter((x) => x.operator == false && x.not == false) + .map((x) => x.items) + .flat() + .map((x) => x?.id ?? x), + [`${field}_or_not`]: fieldlist + .filter((x) => x.operator == true && x.not == true) + .map((x) => x.items) + .flat() + .map((x) => x?.id ?? x), + [`${field}_and_not`]: fieldlist + .filter((x) => x.operator == false && x.not == true) + .map((x) => x.items) + .flat() + .map((x) => x?.id ?? x), + } + }, }, } - + diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index 7166bebf..f3e8d948 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -308,5 +308,6 @@ "show_books": "Show Books", "show_rating": "Show Rating", "show_units": "Show Units", - "show_filters": "Show Filters" + "show_filters": "Show Filters", + "not": "not" } diff --git a/vue/src/utils/models.js b/vue/src/utils/models.js index 01a8e720..c4845aed 100644 --- a/vue/src/utils/models.js +++ b/vue/src/utils/models.js @@ -433,7 +433,26 @@ export class Models { name: "Recipe", apiName: "Recipe", list: { - params: ["query", "keywords", "foods", "units", "rating", "books", "keywordsOr", "foodsOr", "booksOr", "internal", "random", "_new", "page", "pageSize", "options"], + params: [ + "query", + "keywords", + "keywords_or", + "keywords_and", + "keywords_or_not", + "keywords_and_not", + "foods", + "units", + "rating", + "books", + "foodsOr", + "booksOr", + "internal", + "random", + "_new", + "page", + "pageSize", + "options", + ], // 'config': { // 'foods': {'type': 'string'}, // 'keywords': {'type': 'string'}, diff --git a/vue/src/utils/openapi/api.ts b/vue/src/utils/openapi/api.ts index 43b5182c..0d78c830 100644 --- a/vue/src/utils/openapi/api.ts +++ b/vue/src/utils/openapi/api.ts @@ -3054,12 +3054,15 @@ export interface UserPreference { * @memberof UserPreference */ shopping_add_onhand?: boolean; +<<<<<<< HEAD /** * * @type {boolean} * @memberof UserPreference */ left_handed?: boolean; +======= +>>>>>>> complex keyword filters } /** @@ -5279,12 +5282,15 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) /** * * @param {string} [query] Query string matched (fuzzy) against recipe name. In the future also fulltext search. - * @param {number} [keywords] ID of keyword a recipe should have. For multiple repeat parameter. + * @param {number} [keywords] ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or + * @param {number} [keywordsOr] Keyword IDs, repeat for multiple Return recipes with any of the keywords + * @param {number} [keywordsAnd] Keyword IDs, repeat for multiple Return recipes with all of the keywords. + * @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} [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} [keywordsOr] If recipe should have all (AND=false) or any (OR=<b>true</b>) of the provided keywords. * @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>] @@ -5295,7 +5301,7 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listRecipes: async (query?: string, keywords?: number, foods?: number, units?: number, rating?: number, books?: string, keywordsOr?: 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, units?: number, rating?: number, books?: string, foodsOr?: 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); @@ -5316,6 +5322,22 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) localVarQueryParameter['keywords'] = keywords; } + if (keywordsOr !== undefined) { + localVarQueryParameter['keywords_or'] = keywordsOr; + } + + if (keywordsAnd !== undefined) { + localVarQueryParameter['keywords_and'] = keywordsAnd; + } + + if (keywordsOrNot !== undefined) { + localVarQueryParameter['keywords_or_not'] = keywordsOrNot; + } + + if (keywordsAndNot !== undefined) { + localVarQueryParameter['keywords_and_not'] = keywordsAndNot; + } + if (foods !== undefined) { localVarQueryParameter['foods'] = foods; } @@ -5332,10 +5354,6 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) localVarQueryParameter['books'] = books; } - if (keywordsOr !== undefined) { - localVarQueryParameter['keywords_or'] = keywordsOr; - } - if (foodsOr !== undefined) { localVarQueryParameter['foods_or'] = foodsOr; } @@ -9669,12 +9687,15 @@ export const ApiApiFp = function(configuration?: Configuration) { /** * * @param {string} [query] Query string matched (fuzzy) against recipe name. In the future also fulltext search. - * @param {number} [keywords] ID of keyword a recipe should have. For multiple repeat parameter. + * @param {number} [keywords] ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or + * @param {number} [keywordsOr] Keyword IDs, repeat for multiple Return recipes with any of the keywords + * @param {number} [keywordsAnd] Keyword IDs, repeat for multiple Return recipes with all of the keywords. + * @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} [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} [keywordsOr] If recipe should have all (AND=false) or any (OR=<b>true</b>) of the provided keywords. * @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>] @@ -9685,8 +9706,8 @@ export const ApiApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async listRecipes(query?: string, keywords?: number, foods?: number, units?: number, rating?: number, books?: string, keywordsOr?: 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, foods, units, rating, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options); + 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); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -11354,12 +11375,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: /** * * @param {string} [query] Query string matched (fuzzy) against recipe name. In the future also fulltext search. - * @param {number} [keywords] ID of keyword a recipe should have. For multiple repeat parameter. + * @param {number} [keywords] ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or + * @param {number} [keywordsOr] Keyword IDs, repeat for multiple Return recipes with any of the keywords + * @param {number} [keywordsAnd] Keyword IDs, repeat for multiple Return recipes with all of the keywords. + * @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} [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} [keywordsOr] If recipe should have all (AND=false) or any (OR=<b>true</b>) of the provided keywords. * @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>] @@ -11370,8 +11394,8 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listRecipes(query?: string, keywords?: number, foods?: number, units?: number, rating?: number, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): AxiosPromise { - return localVarFp.listRecipes(query, keywords, foods, units, rating, books, keywordsOr, 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, 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)); }, /** * @@ -13063,12 +13087,15 @@ export class ApiApi extends BaseAPI { /** * * @param {string} [query] Query string matched (fuzzy) against recipe name. In the future also fulltext search. - * @param {number} [keywords] ID of keyword a recipe should have. For multiple repeat parameter. + * @param {number} [keywords] ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or + * @param {number} [keywordsOr] Keyword IDs, repeat for multiple Return recipes with any of the keywords + * @param {number} [keywordsAnd] Keyword IDs, repeat for multiple Return recipes with all of the keywords. + * @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} [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} [keywordsOr] If recipe should have all (AND=false) or any (OR=<b>true</b>) of the provided keywords. * @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>] @@ -13080,8 +13107,8 @@ export class ApiApi extends BaseAPI { * @throws {RequiredError} * @memberof ApiApi */ - public listRecipes(query?: string, keywords?: number, foods?: number, units?: number, rating?: number, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any) { - return ApiApiFp(this.configuration).listRecipes(query, keywords, foods, units, rating, books, keywordsOr, 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, 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)); } /**