From f7eabfe45837316c266e8e2199229fa238a0e6d0 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Mon, 31 Mar 2025 08:58:32 +0200 Subject: [PATCH] implemented all filters in frontend --- cookbook/helper/recipe_search.py | 78 ++-- vue3/src/pages/SearchPage.vue | 620 +++++++++++++++++-------------- 2 files changed, 381 insertions(+), 317 deletions(-) diff --git a/cookbook/helper/recipe_search.py b/cookbook/helper/recipe_search.py index b80b1e8e7..081f3fb1c 100644 --- a/cookbook/helper/recipe_search.py +++ b/cookbook/helper/recipe_search.py @@ -206,9 +206,9 @@ class RecipeSearch(): else: order += default_order order[:] = [Lower('name').asc() if x == - 'name' else x for x in order] + 'name' else x for x in order] order[:] = [Lower('name').desc() if x == - '-name' else x for x in order] + '-name' else x for x in order] self.orderby = order def string_filters(self, string=None): @@ -261,49 +261,41 @@ class RecipeSearch(): if self._cookedon_lte: self._queryset = self._queryset.filter(lastcooked__date__lte=self._cookedon_lte).exclude(lastcooked=default) elif self._cookedon_gte: - self._queryset = self._queryset.filter(lastcooked__date__gte=self._cookedon_gte ).exclude(lastcooked=default) + self._queryset = self._queryset.filter(lastcooked__date__gte=self._cookedon_gte).exclude(lastcooked=default) - def _created_on_filter(self, created_date=None): - if created_date is None: - return - lessthan = '-' in created_date[:1] - created_date = date(*[int(x) for x in created_date.split('-') if x != '']) - if lessthan: - self._queryset = self._queryset.filter(created_at__date__lte=created_date) - else: - self._queryset = self._queryset.filter(created_at__date__gte=created_date) + def _viewed_on_filter(self, viewed_date=None): + if self._sort_includes('lastviewed') or self._viewedon_gte or self._viewedon_lte: + longTimeAgo = timezone.now() - timedelta(days=100000) + self._queryset = self._queryset.annotate( + lastviewed=Coalesce(Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo)) + ) + + if self._viewedon_lte: + self._queryset = self._queryset.filter(lastviewed__date__lte=self._viewedon_lte).exclude(lastviewed=longTimeAgo) + elif self._viewedon_gte: + self._queryset = self._queryset.filter(lastviewed__date__gte=self._viewedon_gte).exclude(lastviewed=longTimeAgo) + + def _created_on_filter(self): + if self._createdon: + self._queryset = self._queryset.filter(created_at__date=self._createdon) + elif self._createdon_lte: + self._queryset = self._queryset.filter(created_at__date__lte=self._createdon_lte) + elif self._createdon_gte: + self._queryset = self._queryset.filter(created_at__date__gte=self._createdon_gte) + + def _updated_on_filter(self): + if self._updatedon: + self._queryset = self._queryset.filter(updated_at__date__date=self._updatedon) + elif self._updatedon_lte: + self._queryset = self._queryset.filter(updated_at__date__lte=self._updatedon_lte) + elif self._updatedon_gte: + self._queryset = self._queryset.filter(updated_at__date__gte=self._updatedon_gte) def _created_by_filter(self, created_by_user_id=None): if created_by_user_id is None: return self._queryset = self._queryset.filter(created_by__id=created_by_user_id) - 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) - self._queryset = self._queryset.annotate( - lastviewed=Coalesce(Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo)) - ) - if viewed_date is None: - return - lessthan = '-' in viewed_date[:1] - viewed_date = date(*[int(x)for x in viewed_date.split('-') if x != '']) - - if lessthan: - self._queryset = self._queryset.filter(lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo) - else: - self._queryset = self._queryset.filter(lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo) - def _new_recipes(self, new_days=7): # TODO make new days a user-setting if not self._new: @@ -545,11 +537,11 @@ class RecipeSearch(): shopping_users = [*self._request.user.get_shopping_share(), self._request.user] onhand_filter = ( - Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand - # or substitute food onhand - | Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users) - | Q(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users)) - | Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users)) + Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand + # or substitute food onhand + | Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users) + | Q(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users)) + | Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users)) ) makenow_recipes = Recipe.objects.annotate( count_food=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__isnull=False), distinct=True), diff --git a/vue3/src/pages/SearchPage.vue b/vue3/src/pages/SearchPage.vue index df8678680..11248370a 100644 --- a/vue3/src/pages/SearchPage.vue +++ b/vue3/src/pages/SearchPage.vue @@ -158,9 +158,8 @@ import {useUserPreferenceStore} from "@/stores/UserPreferenceStore"; import {useRouteQuery} from "@vueuse/router"; import {routeQueryDateTransformer, stringToBool, toNumberArray} from "@/utils/utils"; import RandomIcon from "@/components/display/RandomIcon.vue"; -import {VRating, VSelect, VTextField} from "vuetify/components"; +import {VSelect, VTextField} from "vuetify/components"; import RatingField from "@/components/inputs/RatingField.vue"; -import {DateTime} from "luxon"; const {t} = useI18n() const router = useRouter() @@ -170,278 +169,6 @@ const query = useRouteQuery('query', "") const page = useRouteQuery('page', 1, {transform: Number}) const pageSize = useRouteQuery('pageSize', useUserPreferenceStore().deviceSettings.general_tableItemsPerPage, {transform: Number}) -/** - * all filters available to enable - */ -const filters = ref({ - keywords: { - id: 'keywords', - label: 'Keyword (any)', - hint: 'Any of the given keywords', - enabled: false, - default: [], - is: ModelSelect, - model: 'Keyword', - modelValue: useRouteQuery('keywords', [], {transform: toNumberArray}), - mode: 'tags', - object: false, - searchOnLoad: true - }, - keywordsAnd: { - id: 'keywordsAnd', - label: 'Keyword (all)', - hint: 'All of the given keywords', - enabled: false, - default: [], - is: ModelSelect, - model: 'Keyword', - modelValue: useRouteQuery('keywordsAnd', [], {transform: toNumberArray}), - mode: 'tags', - object: false, - searchOnLoad: true - }, - keywordsOrNot: { - id: 'keywordsOrNot', - label: 'Keyword exclude (any)', - hint: 'Exclude recipes with any of the given keywords', - enabled: false, - default: [], - is: ModelSelect, - model: 'Keyword', - modelValue: useRouteQuery('keywordsOrNot', [], {transform: toNumberArray}), - mode: 'tags', - object: false, - searchOnLoad: true - }, - keywordsAndNot: { - id: 'keywordsAndNot', - label: 'Keyword exclude (all)', - hint: 'Exclude recipes with all of the given keywords', - enabled: false, - default: [], - is: ModelSelect, - model: 'Keyword', - modelValue: useRouteQuery('keywordsAndNot', [], {transform: toNumberArray}), - mode: 'tags', - object: false, - searchOnLoad: true - }, - foods: { - id: 'foods', - label: 'Foods (any)', - hint: 'Any of the given foods', - enabled: false, - default: [], - is: ModelSelect, - model: 'Food', - modelValue: useRouteQuery('foods', [], {transform: toNumberArray}), - mode: 'tags', - object: false, - searchOnLoad: true - }, - foodsAnd: { - id: 'foodsAnd', - label: 'Food (all)', - hint: 'All of the given foods', - enabled: false, - default: [], - is: ModelSelect, - model: 'Food', - modelValue: useRouteQuery('foodsAnd', [], {transform: toNumberArray}), - mode: 'tags', - object: false, - searchOnLoad: true - }, - foodsOrNot: { - id: 'foodsOrNot', - label: 'Food exclude (any)', - hint: 'Exclude recipes with any of the given foods', - enabled: false, - default: [], - is: ModelSelect, - model: 'Food', - modelValue: useRouteQuery('foodsOrNot', [], {transform: toNumberArray}), - mode: 'tags', - object: false, - searchOnLoad: true - }, - foodsAndNot: { - id: 'foodsAndNot', - label: 'Food exclude (all)', - hint: 'Exclude recipes with all of the given foods', - enabled: false, - default: [], - is: ModelSelect, - model: 'Food', - modelValue: useRouteQuery('foodsAndNot', [], {transform: toNumberArray}), - mode: 'tags', - object: false, - searchOnLoad: true - }, - books: { - id: 'books', - label: 'Book (any)', - hint: 'Recipes that are in any of the given books', - enabled: false, - default: [], - is: ModelSelect, - model: 'RecipeBook', - modelValue: useRouteQuery('books', [], {transform: toNumberArray}), - mode: 'tags', - object: false, - searchOnLoad: true - }, - booksAnd: { - id: 'booksAnd', - label: 'Book (all)', - hint: 'Recipes that are in all of the given books', - enabled: false, - default: [], - is: ModelSelect, - model: 'RecipeBook', - modelValue: useRouteQuery('booksAnd', [], {transform: toNumberArray}), - mode: 'tags', - object: false, - searchOnLoad: true - }, - booksOrNot: { - id: 'booksOrNot', - label: 'Book exclude (any)', - hint: 'Exclude recipes with any of the given books', - enabled: false, - default: [], - is: ModelSelect, - model: 'RecipeBook', - modelValue: useRouteQuery('booksOrNot', [], {transform: toNumberArray}), - mode: 'tags', - object: false, - searchOnLoad: true - }, - booksAndNot: { - id: 'booksAndNot', - label: 'Book exclude (all)', - hint: 'Exclude recipes with all of the given books', - enabled: false, - default: [], - is: ModelSelect, - model: 'RecipeBook', - modelValue: useRouteQuery('booksAndNot', [], {transform: toNumberArray}), - mode: 'tags', - object: false, - searchOnLoad: true - }, - createdby: { - id: 'createdby', - label: 'Created By', - hint: 'Recipes created by the selected user', - enabled: false, - default: undefined, - is: ModelSelect, - model: 'User', - modelValue: useRouteQuery('createdby', undefined, {transform: Number}), - mode: 'single', - object: false, - searchOnLoad: true - }, - units: { - id: 'units', - label: 'Unit (any)', - hint: 'Recipes that contain any of the given units', - enabled: false, - default: [], - is: ModelSelect, - model: 'Unit', - modelValue: useRouteQuery('units', [], {transform: toNumberArray}), - mode: 'tags', - object: false, - searchOnLoad: true - }, - internal: { - id: 'internal', - label: 'Hide External', - hint: 'Hide external recipes', - enabled: false, - default: "false", - is: VSelect, - items: [{value: "true", title: 'Yes'}, {value: "false", title: 'No'}], - modelValue: useRouteQuery('internal', "false"), - }, - rating: { - id: 'rating', - label: 'Rating (exact)', - hint: 'Recipes with the exact rating', - enabled: false, - default: undefined, - is: RatingField, - modelValue: useRouteQuery('rating', undefined, {transform: Number}), - }, - ratingGte: { - id: 'ratingGte', - label: 'Rating (>=)', - hint: 'Recipes with the given or a greater rating', - enabled: false, - default: undefined, - is: RatingField, - modelValue: useRouteQuery('ratingGte', undefined, {transform: Number}), - }, - ratingLte: { - id: 'ratingLte', - label: 'Rating (<=)', - hint: 'Recipes with the given or a smaller rating', - enabled: false, - default: undefined, - is: RatingField, - modelValue: useRouteQuery('ratingLte', undefined, {transform: Number}), - }, - timescookedGte: { - id: 'timescookedGte', - label: 'Times Cooked (>=)', - hint: 'Recipes that were cooked at least X times', - enabled: false, - default: undefined, - is: VNumberInput, - modelValue: useRouteQuery('timescookedGte', undefined, {transform: Number}), - }, - timescookedLte: { - id: 'timescookedLte', - label: 'Times Cooked (<=)', - hint: 'Recipes that were cooked at most X times', - enabled: false, - default: undefined, - is: VNumberInput, - modelValue: useRouteQuery('timescookedLte', undefined, {transform: Number}), - }, - makenow: { - id: 'makenow', - label: 'Foods on Hand', - hint: 'Only recipes were all foods (or its substitutes) are marked as on hand', - enabled: false, - default: "false", - is: VSelect, - items: [{value: "true", title: 'Yes'}, {value: "false", title: 'No'}], - modelValue: useRouteQuery('makenow', "false"), - }, - - cookedonGte: { - id: 'cookedonGte', - label: 'Cooked after', - hint: 'Only recipes that were cooked on or after the given date.', - enabled: false, - default: null, - is: VDateInput, - modelValue: useRouteQuery('cookedonGte', null, {transform: routeQueryDateTransformer}), - }, - cookedonLte: { - id: 'cookedonLte', - label: 'Cooked before', - hint: 'Only recipes that were cooked on or before the given date.', - enabled: false, - default: null, - is: VDateInput, - modelValue: useRouteQuery('cookedonLte', null, {transform: routeQueryDateTransformer}), - }, -}) - /** * filters that are not yet enabled */ @@ -885,6 +612,351 @@ function apiRecipeListRequestToCustomFilter() { return customFilterParams } +/** + * all filters available to enable + */ +const filters = ref({ + keywords: { + id: 'keywords', + label: 'Keyword (any)', + hint: 'Any of the given keywords', + enabled: false, + default: [], + is: ModelSelect, + model: 'Keyword', + modelValue: useRouteQuery('keywords', [], {transform: toNumberArray}), + mode: 'tags', + object: false, + searchOnLoad: true + }, + keywordsAnd: { + id: 'keywordsAnd', + label: 'Keyword (all)', + hint: 'All of the given keywords', + enabled: false, + default: [], + is: ModelSelect, + model: 'Keyword', + modelValue: useRouteQuery('keywordsAnd', [], {transform: toNumberArray}), + mode: 'tags', + object: false, + searchOnLoad: true + }, + keywordsOrNot: { + id: 'keywordsOrNot', + label: 'Keyword exclude (any)', + hint: 'Exclude recipes with any of the given keywords', + enabled: false, + default: [], + is: ModelSelect, + model: 'Keyword', + modelValue: useRouteQuery('keywordsOrNot', [], {transform: toNumberArray}), + mode: 'tags', + object: false, + searchOnLoad: true + }, + keywordsAndNot: { + id: 'keywordsAndNot', + label: 'Keyword exclude (all)', + hint: 'Exclude recipes with all of the given keywords', + enabled: false, + default: [], + is: ModelSelect, + model: 'Keyword', + modelValue: useRouteQuery('keywordsAndNot', [], {transform: toNumberArray}), + mode: 'tags', + object: false, + searchOnLoad: true + }, + foods: { + id: 'foods', + label: 'Foods (any)', + hint: 'Any of the given foods', + enabled: false, + default: [], + is: ModelSelect, + model: 'Food', + modelValue: useRouteQuery('foods', [], {transform: toNumberArray}), + mode: 'tags', + object: false, + searchOnLoad: true + }, + foodsAnd: { + id: 'foodsAnd', + label: 'Food (all)', + hint: 'All of the given foods', + enabled: false, + default: [], + is: ModelSelect, + model: 'Food', + modelValue: useRouteQuery('foodsAnd', [], {transform: toNumberArray}), + mode: 'tags', + object: false, + searchOnLoad: true + }, + foodsOrNot: { + id: 'foodsOrNot', + label: 'Food exclude (any)', + hint: 'Exclude recipes with any of the given foods', + enabled: false, + default: [], + is: ModelSelect, + model: 'Food', + modelValue: useRouteQuery('foodsOrNot', [], {transform: toNumberArray}), + mode: 'tags', + object: false, + searchOnLoad: true + }, + foodsAndNot: { + id: 'foodsAndNot', + label: 'Food exclude (all)', + hint: 'Exclude recipes with all of the given foods', + enabled: false, + default: [], + is: ModelSelect, + model: 'Food', + modelValue: useRouteQuery('foodsAndNot', [], {transform: toNumberArray}), + mode: 'tags', + object: false, + searchOnLoad: true + }, + books: { + id: 'books', + label: 'Book (any)', + hint: 'Recipes that are in any of the given books', + enabled: false, + default: [], + is: ModelSelect, + model: 'RecipeBook', + modelValue: useRouteQuery('books', [], {transform: toNumberArray}), + mode: 'tags', + object: false, + searchOnLoad: true + }, + booksAnd: { + id: 'booksAnd', + label: 'Book (all)', + hint: 'Recipes that are in all of the given books', + enabled: false, + default: [], + is: ModelSelect, + model: 'RecipeBook', + modelValue: useRouteQuery('booksAnd', [], {transform: toNumberArray}), + mode: 'tags', + object: false, + searchOnLoad: true + }, + booksOrNot: { + id: 'booksOrNot', + label: 'Book exclude (any)', + hint: 'Exclude recipes with any of the given books', + enabled: false, + default: [], + is: ModelSelect, + model: 'RecipeBook', + modelValue: useRouteQuery('booksOrNot', [], {transform: toNumberArray}), + mode: 'tags', + object: false, + searchOnLoad: true + }, + booksAndNot: { + id: 'booksAndNot', + label: 'Book exclude (all)', + hint: 'Exclude recipes with all of the given books', + enabled: false, + default: [], + is: ModelSelect, + model: 'RecipeBook', + modelValue: useRouteQuery('booksAndNot', [], {transform: toNumberArray}), + mode: 'tags', + object: false, + searchOnLoad: true + }, + createdby: { + id: 'createdby', + label: 'Created By', + hint: 'Recipes created by the selected user', + enabled: false, + default: undefined, + is: ModelSelect, + model: 'User', + modelValue: useRouteQuery('createdby', undefined, {transform: Number}), + mode: 'single', + object: false, + searchOnLoad: true + }, + units: { + id: 'units', + label: 'Unit (any)', + hint: 'Recipes that contain any of the given units', + enabled: false, + default: [], + is: ModelSelect, + model: 'Unit', + modelValue: useRouteQuery('units', [], {transform: toNumberArray}), + mode: 'tags', + object: false, + searchOnLoad: true + }, + internal: { + id: 'internal', + label: 'Hide External', + hint: 'Hide external recipes', + enabled: false, + default: "false", + is: VSelect, + items: [{value: "true", title: 'Yes'}, {value: "false", title: 'No'}], + modelValue: useRouteQuery('internal', "false"), + }, + rating: { + id: 'rating', + label: 'Rating (exact)', + hint: 'Recipes with the exact rating', + enabled: false, + default: undefined, + is: RatingField, + modelValue: useRouteQuery('rating', undefined, {transform: Number}), + }, + ratingGte: { + id: 'ratingGte', + label: 'Rating (>=)', + hint: 'Recipes with the given or a greater rating', + enabled: false, + default: undefined, + is: RatingField, + modelValue: useRouteQuery('ratingGte', undefined, {transform: Number}), + }, + ratingLte: { + id: 'ratingLte', + label: 'Rating (<=)', + hint: 'Recipes with the given or a smaller rating', + enabled: false, + default: undefined, + is: RatingField, + modelValue: useRouteQuery('ratingLte', undefined, {transform: Number}), + }, + timescookedGte: { + id: 'timescookedGte', + label: 'Times Cooked (>=)', + hint: 'Recipes that were cooked at least X times', + enabled: false, + default: undefined, + is: VNumberInput, + modelValue: useRouteQuery('timescookedGte', undefined, {transform: Number}), + }, + timescookedLte: { + id: 'timescookedLte', + label: 'Times Cooked (<=)', + hint: 'Recipes that were cooked at most X times', + enabled: false, + default: undefined, + is: VNumberInput, + modelValue: useRouteQuery('timescookedLte', undefined, {transform: Number}), + }, + makenow: { + id: 'makenow', + label: 'Foods on Hand', + hint: 'Only recipes were all foods (or its substitutes) are marked as on hand', + enabled: false, + default: "false", + is: VSelect, + items: [{value: "true", title: 'Yes'}, {value: "false", title: 'No'}], + modelValue: useRouteQuery('makenow', "false"), + }, + + cookedonGte: { + id: 'cookedonGte', + label: 'Cooked after', + hint: 'Only recipes that were cooked on or after the given date.', + enabled: false, + default: null, + is: VDateInput, + modelValue: useRouteQuery('cookedonGte', null, {transform: routeQueryDateTransformer}), + }, + cookedonLte: { + id: 'cookedonLte', + label: 'Cooked before', + hint: 'Only recipes that were cooked on or before the given date.', + enabled: false, + default: null, + is: VDateInput, + modelValue: useRouteQuery('cookedonLte', null, {transform: routeQueryDateTransformer}), + }, + viewedonGte: { + id: 'viewedonGte', + label: 'Viewed after', + hint: 'Only recipes that were viewed on or after the given date.', + enabled: false, + default: null, + is: VDateInput, + modelValue: useRouteQuery('viewedonGte', null, {transform: routeQueryDateTransformer}), + }, + viewedonLte: { + id: 'viewedonLte', + label: 'Viewed before', + hint: 'Only recipes that were viewed on or before the given date.', + enabled: false, + default: null, + is: VDateInput, + modelValue: useRouteQuery('viewedonLte', null, {transform: routeQueryDateTransformer}), + }, + + createdon: { + id: 'createdon', + label: 'Created on', + hint: 'Only recipes that were created on the given date.', + enabled: false, + default: null, + is: VDateInput, + modelValue: useRouteQuery('createdon', null, {transform: routeQueryDateTransformer}), + }, + createdonGte: { + id: 'createdonGte', + label: 'Created on/after', + hint: 'Only recipes that were created on or after the given date.', + enabled: false, + default: null, + is: VDateInput, + modelValue: useRouteQuery('createdonGte', null, {transform: routeQueryDateTransformer}), + }, + createdonLte: { + id: 'createdonLte', + label: 'Created on/before', + hint: 'Only recipes that were created on or before the given date.', + enabled: false, + default: null, + is: VDateInput, + modelValue: useRouteQuery('createdonLte', null, {transform: routeQueryDateTransformer}), + }, + updatedon: { + id: 'updatedon', + label: 'Updated on', + hint: 'Only recipes that were updated on the given date.', + enabled: false, + default: null, + is: VDateInput, + modelValue: useRouteQuery('updatedon', null, {transform: routeQueryDateTransformer}), + }, + updatedonGte: { + id: 'updatedonGte', + label: 'Updated on/after', + hint: 'Only recipes that were updated on or after the given date.', + enabled: false, + default: null, + is: VDateInput, + modelValue: useRouteQuery('updatedonGte', null, {transform: routeQueryDateTransformer}), + }, + updatedonLte: { + id: 'updatedonLte', + label: 'Updated on/before', + hint: 'Only recipes that were updated on or before the given date.', + enabled: false, + default: null, + is: VDateInput, + modelValue: useRouteQuery('updatedonLte', null, {transform: routeQueryDateTransformer}), + }, +}) +