diff --git a/cookbook/helper/recipe_search.py b/cookbook/helper/recipe_search.py index 0cfbcf6d..5b254572 100644 --- a/cookbook/helper/recipe_search.py +++ b/cookbook/helper/recipe_search.py @@ -72,6 +72,7 @@ class RecipeSearch(): self._timescooked = self._params.get('timescooked', None) self._cookedon = self._params.get('cookedon', None) self._createdon = self._params.get('createdon', None) + self._updatedon = self._params.get('updatedon', None) self._viewedon = self._params.get('viewedon', None) # this supports hidden feature to find recipes missing X ingredients try: @@ -114,6 +115,7 @@ class RecipeSearch(): self._recently_viewed(num_recent=self._num_recent) self._cooked_on_filter(cooked_date=self._cookedon) self._created_on_filter(created_date=self._createdon) + self._updated_on_filter(updated_date=self._updatedon) self._viewed_on_filter(viewed_date=self._viewedon) self._favorite_recipes(timescooked=self._timescooked) self._new_recipes() @@ -232,6 +234,16 @@ class RecipeSearch(): else: self._queryset = self._queryset.filter(created_at__date__gte=created_date) + def _updated_on_filter(self, updated_date=None): + if updated_date is None: + return + lessthan = '-' in updated_date[:1] + updated_date = date(*[int(x) for x in updated_date.split('-') if x != '']) + if lessthan: + self._queryset = self._queryset.filter(updated_at__date__lte=updated_date) + else: + self._queryset = self._queryset.filter(updated_at__date__gte=updated_date) + def _viewed_on_filter(self, viewed_date=None): if self._sort_includes('lastviewed') or viewed_date: longTimeAgo = timezone.now() - timedelta(days=100000) diff --git a/cookbook/tests/factories/__init__.py b/cookbook/tests/factories/__init__.py index 5cc215d3..77b59199 100644 --- a/cookbook/tests/factories/__init__.py +++ b/cookbook/tests/factories/__init__.py @@ -1,4 +1,5 @@ +from datetime import date from decimal import Decimal import factory @@ -9,7 +10,7 @@ from django_scopes import scopes_disabled from faker import Factory as FakerFactory from pytest_factoryboy import register -from cookbook.models import Step +from cookbook.models import Recipe, Step # this code will run immediately prior to creating the model object useful when you want a reverse relationship # log = factory.RelatedFactory( @@ -358,12 +359,13 @@ class RecipeFactory(factory.django.DjangoModelFactory): waiting_time = factory.LazyAttribute(lambda x: faker.random_int(min=0, max=360)) internal = False created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space')) - created_at = factory.LazyAttribute(lambda x: faker.date_this_decade()) + created_at = factory.LazyAttribute(lambda x: faker.date_between_dates(date_start=date(2000, 1, 1), date_end=date(2020, 12, 31))) space = factory.SubFactory(SpaceFactory) @classmethod def _create(cls, target_class, *args, **kwargs): # override create to prevent auto_add_now from changing the created_at date created_at = kwargs.pop('created_at', None) + # updated_at = kwargs.pop('updated_at', None) obj = super(RecipeFactory, cls)._create(target_class, *args, **kwargs) if created_at is not None: obj.created_at = created_at diff --git a/cookbook/tests/other/test_recipe_full_text_search.py b/cookbook/tests/other/test_recipe_full_text_search.py index b9da134b..85d24b70 100644 --- a/cookbook/tests/other/test_recipe_full_text_search.py +++ b/cookbook/tests/other/test_recipe_full_text_search.py @@ -87,9 +87,15 @@ def found_recipe(request, space_1, accent, unaccent, u1_s1, u2_s1): days_3 = timezone.now() - timedelta(days=3) days_15 = timezone.now() - timedelta(days=15) days_30 = timezone.now() - timedelta(days=30) - recipe1 = RecipeFactory.create(space=space_1) - recipe2 = RecipeFactory.create(space=space_1) - recipe3 = RecipeFactory.create(space=space_1) + if request.param.get('createdon', None): + recipe1 = RecipeFactory.create(space=space_1, created_at=days_3) + recipe2 = RecipeFactory.create(space=space_1, created_at=days_30) + recipe3 = RecipeFactory.create(space=space_1, created_at=days_15) + + else: + recipe1 = RecipeFactory.create(space=space_1) + recipe2 = RecipeFactory.create(space=space_1) + recipe3 = RecipeFactory.create(space=space_1) obj1 = None obj2 = None @@ -137,13 +143,7 @@ def found_recipe(request, space_1, accent, unaccent, u1_s1, u2_s1): i2.instruction = accent i1.save() i2.save() - if request.param.get('createdon', None): - recipe1.created_at = days_3 - recipe2.created_at = days_30 - recipe3.created_at = days_15 - recipe1.save() - recipe2.save() - recipe3.save() + if request.param.get('viewedon', None): ViewLogFactory.create(recipe=recipe1, created_by=user1, created_at=days_3, space=space_1) ViewLogFactory.create(recipe=recipe2, created_by=user1, created_at=days_30, space=space_1) @@ -291,8 +291,14 @@ def test_search_string(found_recipe, recipes, user1, space_1): ({'viewedon': True}, 'viewedon', (1, 1)), ({'cookedon': True}, 'cookedon', (1, 1)), ({'createdon': True}, 'createdon', (2, 12)), # created dates are not filtered by user + ({'createdon': True}, 'updatedon', (2, 12)), # updated dates are not filtered by user ], indirect=['found_recipe']) def test_search_date(found_recipe, recipes, param_type, result, u1_s1, u2_s1, space_1): + # force updated_at to equal created_at datetime + with scope(space=space_1): + for recipe in Recipe.objects.all(): + Recipe.objects.filter(id=recipe.id).update(updated_at=recipe.created_at) + date = (timezone.now() - timedelta(days=15)).strftime("%Y-%m-%d") param1 = f"?{param_type}={date}" param2 = f"?{param_type}=-{date}" diff --git a/cookbook/views/api.py b/cookbook/views/api.py index a2c4b697..0f256589 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -654,6 +654,7 @@ class RecipeViewSet(viewsets.ModelViewSet): QueryParam(name='timescooked', description=_('Filter recipes cooked X times or more. Negative values returns cooked less than X times'), qtype='int'), QueryParam(name='cookedon', description=_('Filter recipes last cooked on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')), QueryParam(name='createdon', description=_('Filter recipes created on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')), + QueryParam(name='updatedon', description=_('Filter recipes updated on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')), QueryParam(name='viewedon', description=_('Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')), QueryParam(name='makenow', description=_('Filter recipes that can be made with OnHand food. [''true''/''false'']')), ] diff --git a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue index 9892760d..32062d73 100644 --- a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue +++ b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue @@ -107,21 +107,24 @@ - + - + - + - + - + + + + @@ -461,6 +464,24 @@ + + + + + >= + <= + + + {{ $t("make_now") }} @@ -481,7 +502,7 @@
{{ $t("explain") }}
- +
{{ $t("expert_mode") }} @@ -698,6 +719,8 @@ export default { cookedon_gte: true, createdon: undefined, createdon_gte: true, + updatedon: undefined, + updatedon_gte: true, viewedon: undefined, viewedon_gte: true, sort_order: [], @@ -733,6 +756,7 @@ export default { show_cookedon: false, show_viewedon: false, show_createdon: false, + show_updatedon: false, include_children: true, }, pagination_count: 0, @@ -1130,6 +1154,10 @@ export default { if (viewedon !== undefined && !this.search.viewedon_gte) { viewedon = "-" + viewedon } + let updatedon = this.search.updatedon || undefined + if (updatedon !== undefined && !this.search.updatedon_gte) { + updatedon = "-" + updatedon + } let timescooked = parseInt(this.search.timescooked) if (isNaN(timescooked)) { timescooked = undefined @@ -1151,6 +1179,7 @@ export default { makenow: this.search.makenow || undefined, cookedon: cookedon, createdon: createdon, + updatedon: updatedon, viewedon: viewedon, page: this.search.pagination_page, pageSize: this.ui.page_size, @@ -1181,7 +1210,10 @@ export default { this.search?.search_rating !== undefined || (this.search.timescooked !== undefined && this.search.timescooked !== "") || this.search.makenow !== false || - (this.search.cookedon !== undefined && this.search.cookedon !== "") + (this.search.cookedon !== undefined && this.search.cookedon !== "") || + (this.search.viewedon !== undefined && this.search.viewedon !== "") || + (this.search.createdon !== undefined && this.search.createdon !== "") || + (this.search.updatedon !== undefined && this.search.updatedon !== "") if (ignore_string) { return filtered diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index 88d1333e..4895cd87 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -340,5 +340,6 @@ "ChildInheritFields_help": "Children will inherit these fields by default.", "InheritFields_help": "The values of these fields will be inheritted from parent (Exception: blank shopping categories are not inheritted)", "last_viewed": "Last Viewed", - "created_on": "Created On" + "created_on": "Created On", + "updatedon": "Updated On" } diff --git a/vue/src/utils/models.js b/vue/src/utils/models.js index a977b0e9..4c30205b 100644 --- a/vue/src/utils/models.js +++ b/vue/src/utils/models.js @@ -532,6 +532,7 @@ export class Models { "timescooked", "cookedon", "createdon", + "updatedon", "viewedon", "makenow", "page", diff --git a/vue/src/utils/openapi/api.ts b/vue/src/utils/openapi/api.ts index 7e75aca5..07281335 100644 --- a/vue/src/utils/openapi/api.ts +++ b/vue/src/utils/openapi/api.ts @@ -5765,6 +5765,7 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) * @param {number} [timescooked] Filter recipes cooked X times or more. Negative values returns cooked less than X times * @param {string} [cookedon] Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on or before date. * @param {string} [createdon] Filter recipes created on or after YYYY-MM-DD. Prepending - filters on or before date. + * @param {string} [updatedon] Filter recipes updated on or after YYYY-MM-DD. Prepending - filters on or before date. * @param {string} [viewedon] Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending - filters on or before date. * @param {string} [makenow] Filter recipes that can be made with OnHand food. [true/<b>false</b>] * @param {number} [page] A page number within the paginated result set. @@ -5772,7 +5773,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, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, cookedon?: string, createdon?: string, viewedon?: string, makenow?: 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?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, cookedon?: string, createdon?: string, updatedon?: string, viewedon?: string, makenow?: 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); @@ -5881,6 +5882,10 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) localVarQueryParameter['createdon'] = createdon; } + if (updatedon !== undefined) { + localVarQueryParameter['updatedon'] = updatedon; + } + if (viewedon !== undefined) { localVarQueryParameter['viewedon'] = viewedon; } @@ -10499,6 +10504,7 @@ export const ApiApiFp = function(configuration?: Configuration) { * @param {number} [timescooked] Filter recipes cooked X times or more. Negative values returns cooked less than X times * @param {string} [cookedon] Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on or before date. * @param {string} [createdon] Filter recipes created on or after YYYY-MM-DD. Prepending - filters on or before date. + * @param {string} [updatedon] Filter recipes updated on or after YYYY-MM-DD. Prepending - filters on or before date. * @param {string} [viewedon] Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending - filters on or before date. * @param {string} [makenow] Filter recipes that can be made with OnHand food. [true/<b>false</b>] * @param {number} [page] A page number within the paginated result set. @@ -10506,8 +10512,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, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, cookedon?: string, createdon?: string, viewedon?: string, makenow?: 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, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, cookedon, createdon, viewedon, makenow, 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?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, cookedon?: string, createdon?: string, updatedon?: string, viewedon?: string, makenow?: 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, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, cookedon, createdon, updatedon, viewedon, makenow, page, pageSize, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -12316,6 +12322,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: * @param {number} [timescooked] Filter recipes cooked X times or more. Negative values returns cooked less than X times * @param {string} [cookedon] Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on or before date. * @param {string} [createdon] Filter recipes created on or after YYYY-MM-DD. Prepending - filters on or before date. + * @param {string} [updatedon] Filter recipes updated on or after YYYY-MM-DD. Prepending - filters on or before date. * @param {string} [viewedon] Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending - filters on or before date. * @param {string} [makenow] Filter recipes that can be made with OnHand food. [true/<b>false</b>] * @param {number} [page] A page number within the paginated result set. @@ -12323,8 +12330,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, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, cookedon?: string, createdon?: string, viewedon?: string, makenow?: 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, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, cookedon, createdon, viewedon, makenow, 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?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, cookedon?: string, createdon?: string, updatedon?: string, viewedon?: string, makenow?: 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, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, cookedon, createdon, updatedon, viewedon, makenow, page, pageSize, options).then((request) => request(axios, basePath)); }, /** * @@ -14163,6 +14170,7 @@ export class ApiApi extends BaseAPI { * @param {number} [timescooked] Filter recipes cooked X times or more. Negative values returns cooked less than X times * @param {string} [cookedon] Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on or before date. * @param {string} [createdon] Filter recipes created on or after YYYY-MM-DD. Prepending - filters on or before date. + * @param {string} [updatedon] Filter recipes updated on or after YYYY-MM-DD. Prepending - filters on or before date. * @param {string} [viewedon] Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending - filters on or before date. * @param {string} [makenow] Filter recipes that can be made with OnHand food. [true/<b>false</b>] * @param {number} [page] A page number within the paginated result set. @@ -14171,8 +14179,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, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, cookedon?: string, createdon?: string, viewedon?: string, makenow?: 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, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, cookedon, createdon, viewedon, makenow, 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?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, cookedon?: string, createdon?: string, updatedon?: string, viewedon?: string, makenow?: 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, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, cookedon, createdon, updatedon, viewedon, makenow, page, pageSize, options).then((request) => request(this.axios, this.basePath)); } /**