diff --git a/cookbook/helper/recipe_search.py b/cookbook/helper/recipe_search.py index 018368ae..a7ff1f5c 100644 --- a/cookbook/helper/recipe_search.py +++ b/cookbook/helper/recipe_search.py @@ -56,10 +56,7 @@ class RecipeSearch(): # TODO add created before/after # TODO image exists self._sort_order = self._params.get('sort_order', None) - # TODO add save - self._books_or = str2bool(self._params.get('books_or', True)) - self._internal = str2bool(self._params.get('internal', False)) self._random = str2bool(self._params.get('random', False)) self._new = str2bool(self._params.get('new', False)) @@ -88,7 +85,7 @@ class RecipeSearch(): self.search_rank = ( SearchRank('name_search_vector', self.search_query, cover_density=True) + SearchRank('desc_search_vector', self.search_query, cover_density=True) - + SearchRank('steps__search_vector', self.search_query, cover_density=True) + # + SearchRank('steps__search_vector', self.search_query, cover_density=True) # Is a large performance drag ) self.orderby = [] self._default_sort = ['-favorite'] # TODO add user setting @@ -97,11 +94,11 @@ class RecipeSearch(): def get_queryset(self, queryset): self._queryset = queryset - self.recently_viewed_recipes(self._last_viewed) + self._build_sort_order() + self._recently_viewed(num_recent=self._last_viewed) + self._last_cooked() self._favorite_recipes() self._new_recipes() - # self._last_viewed() - # self._last_cooked() self.keyword_filters(**self._keywords) self.food_filters(**self._foods) self.book_filters(**self._books) @@ -111,35 +108,48 @@ class RecipeSearch(): self.unit_filters(units=self._units) self.string_filters(string=self._string) # self._queryset = self._queryset.distinct() # TODO 2x check. maybe add filter of recipe__in after orderby - self._apply_order_by() - return self._queryset.filter(space=self._request.space) + return self._queryset.filter(space=self._request.space).order_by(*self.orderby) - def _apply_order_by(self): + def _sort_includes(self, *args): + for x in args: + if x in self.orderby: + return True + elif '-' + x in self.orderby: + return True + return False + + def _build_sort_order(self): if self._random: self._queryset = self._queryset.order_by("?") else: - if self._sort_order: - self._queryset.order_by(*self._sort_order) - return - - order = [] # TODO add user preferences here: name, date cooked, rating, times cooked, date created, date viewed, random - if '-recent' in self.orderby and self._last_viewed: + order = [] + # TODO add userpreference for default sort order and replace '-favorite' + default_order = ['-favorite'] + # recent and new_recipe are always first; they float a few recipes to the top + if self._last_viewed: order += ['-recent'] - if '-new_recipe' in self.orderby and self._new: + if self._new: order += ['-new_recipe'] - if '-rank' in self.orderby and '-simularity' in self.orderby: - self._queryset = self._queryset.annotate(score=Sum(F('rank')+F('simularity'))) - order += ['-score'] - elif '-rank' in self.orderby: - self._queryset = self._queryset.annotate(score=F('rank')) - order += ['-score'] - elif '-simularity' in self.orderby: - self._queryset = self._queryset.annotate(score=F('simularity')) - order += ['-score'] - for x in list(set(self.orderby)-set([*order, '-rank', '-simularity'])): - order += [x] - self._queryset = self._queryset.order_by(*order) + # if a sort order is provided by user - use that order + if self._sort_order: + + if not isinstance(self._sort_order, list): + order += [self._sort_order] + else: + order += self._sort_order + if not self._postgres or not self._string: + if 'score' in order: + order.remove('score') + if '-score' in order: + order.remove('-score') + # if no sort order provided prioritize text search, followed by the default search + elif self._postgres and self._string: + order += ['-score', *default_order] + # otherwise sort by the remaining order_by attributes or favorite by default + else: + order += default_order + self.orderby = order def string_filters(self, string=None): if not string: @@ -157,19 +167,29 @@ class RecipeSearch(): query_filter |= f else: query_filter = f - self._queryset = self._queryset.filter(query_filter).distinct() - # TODO add annotation for simularity + self._queryset = self._queryset.filter(query_filter) if self._fulltext_include: - self._queryset = self._queryset.annotate(rank=self.search_rank) - self.orderby += ['-rank'] + if self._fuzzy_match is None: + self._queryset = self._queryset.annotate(score=self.search_rank) + else: + self._queryset = self._queryset.annotate(rank=self.search_rank) - if self._fuzzy_match is not None: # this annotation is full text, not trigram + if self._fuzzy_match is not None: simularity = self._fuzzy_match.filter(pk=OuterRef('pk')).values('simularity') - self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0)) - self.orderby += ['-simularity'] + if not self._fulltext_include: + self._queryset = self._queryset.annotate(score=Coalesce(Subquery(simularity), 0.0)) + else: + self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0)) + if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None: + self._queryset = self._queryset.annotate(score=Sum(F('rank')+F('simularity'))) else: self._queryset = self._queryset.filter(name__icontains=self._string) + def _last_cooked(self): + if self._sort_includes('lastcooked'): + self._queryset = self._queryset.annotate(lastcooked=Coalesce( + Max(Case(When(created_by=self._request.user, space=self._request.space, then='cooklog__pk'))), Value(0))) + def _new_recipes(self, new_days=7): # TODO make new days a user-setting if not self._new: @@ -178,23 +198,23 @@ class RecipeSearch(): self._queryset.annotate(new_recipe=Case( When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')), default=Value(0), )) ) - self.orderby += ['-new_recipe'] - def recently_viewed_recipes(self, last_viewed=None): - if not last_viewed: + def _recently_viewed(self, num_recent=None): + if not num_recent: + if self._sort_includes('lastviewed'): + self._queryset = self._queryset.annotate(lastviewed=Coalesce( + Max(Case(When(created_by=self._request.user, space=self._request.space, then='viewlog__pk'))), Value(0))) return last_viewed_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space).values( - 'recipe').annotate(recent=Max('created_at')).order_by('-recent') - last_viewed_recipes = last_viewed_recipes[:last_viewed] - self.orderby += ['-recent'] + 'recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent] self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=last_viewed_recipes.values('recipe'), then='viewlog__pk'))), Value(0))) def _favorite_recipes(self): - self.orderby += ['-favorite'] # default sort? - favorite_recipes = CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk') - ).values('recipe').annotate(count=Count('pk', distinct=True)).values('count') - self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), 0)) + if self._sort_includes('favorite'): + favorite_recipes = CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk') + ).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, **kwargs): if all([kwargs[x] is None for x in kwargs]): @@ -258,12 +278,13 @@ class RecipeSearch(): self._queryset = self._queryset.filter(steps__ingredients__unit__in=units) def rating_filter(self, rating=None): + if rating or self._sort_includes('rating'): + # TODO make ratings a settings user-only vs all-users + self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0))))) if rating is None: return lessthan = '-' in rating - # TODO make ratings a settings user-only vs all-users - self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0))))) if rating == 0: self._queryset = self._queryset.filter(rating=0) elif lessthan: diff --git a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue index cf703fda..3d8639f0 100644 --- a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue +++ b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue @@ -112,6 +112,9 @@ + + + @@ -140,6 +143,37 @@ + +
+
+ + + + +
+
{{ $t("Keywords") }}
@@ -429,17 +463,18 @@ import GenericMultiselect from "@/components/GenericMultiselect" import { Treeselect, LOAD_CHILDREN_OPTIONS } from "@riophae/vue-treeselect" import "@riophae/vue-treeselect/dist/vue-treeselect.css" import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher" +import Multiselect from "vue-multiselect" Vue.use(VueCookies) Vue.use(BootstrapVue) -let SEARCH_COOKIE_NAME = "search_settings1" +let SEARCH_COOKIE_NAME = "search_settings2" let UI_COOKIE_NAME = "ui_search_settings" export default { name: "RecipeSearchView", mixins: [ResolveUrlMixin, ApiMixin, ToastMixin], - components: { GenericMultiselect, RecipeCard, Treeselect, RecipeSwitcher }, + components: { GenericMultiselect, RecipeCard, Treeselect, RecipeSwitcher, Multiselect }, data() { return { // this.Models and this.Actions inherited from ApiMixin @@ -474,6 +509,8 @@ export default { search_rating: undefined, search_rating_gte: true, search_units_or: true, + search_filter: undefined, + sort_order: [], pagination_page: 1, expert_mode: false, keywords_fields: 1, @@ -499,11 +536,11 @@ export default { show_rating: true, show_units: false, show_filters: false, + show_sortby: false, }, pagination_count: 0, random_search: false, debug: false, - custom_filters: [], } }, computed: { @@ -565,6 +602,35 @@ export default { unitFields: function () { return !this.expertMode ? 1 : this.search.units_fields }, + sortOptions: function () { + let sort_order = [] + let x = 1 + const field = [ + [this.$t("search_rank"), "score"], + [this.$t("Name"), "name"], + [this.$t("date_cooked"), "lastcooked"], + [this.$t("Rating"), "rating"], + [this.$t("times_cooked"), "favorite"], + [this.$t("date_created"), "created_at"], + [this.$t("date_viewed"), "lastviewed"], + ] + field.forEach((f) => { + sort_order.push( + { + id: x, + text: `${f[0]} ↑`, + value: f[1], + }, + { + id: x + 1, + text: `${f[0]} ↓`, + value: `-${f[1]}`, + } + ) + x = x + 2 + }) + return sort_order + }, }, mounted() { this.$nextTick(function () { @@ -572,7 +638,7 @@ export default { this.ui = Object.assign({}, this.ui, this.$cookies.get(UI_COOKIE_NAME)) } if (this.ui.remember_search && this.$cookies.isKey(SEARCH_COOKIE_NAME)) { - this.search = Object.assign({}, this.search, this.$cookies.get(SEARCH_COOKIE_NAME), `${this.ui.remember_hours}h`) + this.search = Object.assign({}, this.search, this.$cookies.get(SEARCH_COOKIE_NAME)) } let urlParams = new URLSearchParams(window.location.search) @@ -644,8 +710,8 @@ export default { this.refreshData(false) }, "ui.tree_select": function () { - if (this.ui.tree_select && (!this.facets?.Keywords || !this.facets?.Foods)) { - this.getFacets(this.facets?.hash) + if (this.ui.tree_select && (this.facets?.Keywords.length == 0 || this.facets?.Foods.length == 0) && this.facets?.cache_key) { + this.getFacets(this.facets?.cache_key) } }, "search.search_input": _debounce(function () { @@ -656,6 +722,10 @@ export default { "ui.page_size": _debounce(function () { this.refreshData(false) }, 300), + "search.search_filter": _debounce(function () { + // TODO clear existing filters + this.refreshData(false) + }, 300), }, methods: { // this.genericAPI inherited from ApiMixin @@ -728,6 +798,8 @@ export default { }) this.search.search_units = [] this.search.search_rating = undefined + this.search.search_filter = undefined + this.search.sort_order = undefined this.search.pagination_page = 1 this.search.keywords_fields = 1 this.search.foods_fields = 1 @@ -792,6 +864,9 @@ export default { } }, buildParams: function (random) { + if (this.search.search_filter) { + return JSON.parse(this.search.search_filter.search) + } this.random_search = random let rating = this.search.search_rating if (rating !== undefined && !this.search.search_rating_gte) { @@ -811,9 +886,13 @@ export default { page: this.search.pagination_page, pageSize: this.ui.page_size, } + + let query = { sort_order: this.search.sort_order.map((x) => x.value) } if (this.searchFiltered()) { - params.options = { query: { last_viewed: this.ui.recently_viewed } } + query.last_viewed = this.ui.recently_viewed } + params.options = { query: query } + console.log(params) return params }, searchFiltered: function (ignore_string = false) { @@ -865,7 +944,6 @@ export default { this.genericAPI(this.Models.CUSTOM_FILTER, this.Actions.CREATE, params) .then((result) => { StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE) - console.log("you saved: ", filtername, this.buildParams(false), result) }) .catch((err) => { console.log(err, Object.keys(err)) diff --git a/vue/src/components/Buttons/RecipeSwitcher.vue b/vue/src/components/Buttons/RecipeSwitcher.vue index b6d8daf2..57a233c6 100644 --- a/vue/src/components/Buttons/RecipeSwitcher.vue +++ b/vue/src/components/Buttons/RecipeSwitcher.vue @@ -1,22 +1,25 @@ - -
- - diff --git a/vue/src/components/Modals/ShoppingModal.vue b/vue/src/components/Modals/ShoppingModal.vue index e2d29300..57256764 100644 --- a/vue/src/components/Modals/ShoppingModal.vue +++ b/vue/src/components/Modals/ShoppingModal.vue @@ -97,6 +97,7 @@ export default { } }, mounted() { + console.log("shopping modal") this.recipe_servings = this.servings }, computed: { diff --git a/vue/src/components/RecipeContextMenu.vue b/vue/src/components/RecipeContextMenu.vue index 1bd7531e..48d52069 100644 --- a/vue/src/components/RecipeContextMenu.vue +++ b/vue/src/components/RecipeContextMenu.vue @@ -100,7 +100,7 @@ export default { return { servings_value: 0, recipe_share_link: undefined, - modal_id: this.recipe.id + Math.round(Math.random() * 100000), + modal_id: undefined, options: { entryEditing: { date: null, @@ -200,6 +200,7 @@ export default { navigator.share(shareData) }, addToShopping() { + this.modal_id = this.recipe.id + Math.round(Math.random() * 100000) this.$bvModal.show(`shopping_${this.modal_id}`) }, }, diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index b8e29701..eda97ba9 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -315,5 +315,14 @@ "left_handed": "Left-handed mode", "left_handed_help": "Will optimize the UI for use with your left hand.", "Custom Filter": "Custom Filter", - "shared_with": "Shared With" + "shared_with": "Shared With", + "sort_by": "Sort By", + "asc": "Ascending", + "desc": "Descending", + "date_viewed": "Last Viewed", + "date_cooked": "Last Cooked", + "times_cooked": "Times Cooked", + "date_created": "Date Created", + "show_sortby": "Show Sort By", + "search_rank": "Search Rank" }