consolidated recently viewed into FTS queryset

This commit is contained in:
smilerz
2021-08-25 21:08:35 -05:00
parent aa2d0eafb1
commit 2a1e1953ee
7 changed files with 73 additions and 96 deletions

View File

@ -8,7 +8,7 @@ from django.db.models import Count, Max, Q, Subquery, Case, When, Value
from django.utils import translation from django.utils import translation
from cookbook.managers import DICTIONARY from cookbook.managers import DICTIONARY
from cookbook.models import Food, Keyword, ViewLog from cookbook.models import Food, Keyword, Recipe, ViewLog
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected # TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
@ -30,26 +30,28 @@ def search_recipes(request, queryset, params):
search_random = params.get('random', False) search_random = params.get('random', False)
search_new = params.get('new', False) search_new = params.get('new', False)
search_last_viewed = int(params.get('last_viewed', 0)) search_last_viewed = int(params.get('last_viewed', 0))
orderby = []
# TODO update this to concat with full search queryset qs1 | qs2 # TODO update this to concat with full search queryset qs1 | qs2
if search_last_viewed > 0: if search_last_viewed > 0:
last_viewed_recipes = ViewLog.objects.filter( last_viewed_recipes = ViewLog.objects.filter(
created_by=request.user, space=request.space, created_by=request.user, space=request.space,
created_at__gte=datetime.now() - timedelta(days=14) created_at__gte=datetime.now() - timedelta(days=14) # TODO make recent days a setting
).order_by('-pk').values_list('recipe__pk', flat=True) ).order_by('-pk').values_list('recipe__pk', flat=True)
last_viewed_recipes = list(dict.fromkeys(last_viewed_recipes))[:search_last_viewed] # removes duplicates from list prior to slicing last_viewed_recipes = list(dict.fromkeys(last_viewed_recipes))[:search_last_viewed] # removes duplicates from list prior to slicing
return queryset.annotate(last_view=Max('viewlog__pk')).annotate(new=Case(When(pk__in=last_viewed_recipes, then=('last_view')), default=Value(0))).filter(new__gt=0).order_by('-new') # return queryset.annotate(last_view=Max('viewlog__pk')).annotate(new=Case(When(pk__in=last_viewed_recipes, then=('last_view')), default=Value(0))).filter(new__gt=0).order_by('-new')
# queryset that only annotates most recent view (higher pk = lastest view) # queryset that only annotates most recent view (higher pk = lastest view)
# TODO queryset.annotate(last_view=Max('viewlog__pk')).annotate(new=Case(When(pk__in=last_viewed_recipes, then=Value(100)), default=Value(0))).order_by('-new') queryset = queryset.annotate(last_view=Max('viewlog__pk')).annotate(recent=Case(When(pk__in=last_viewed_recipes, then=('last_view')), default=Value(0)))
orderby += ['-recent']
orderby = [] # TODO create setting for default ordering - most cooked, rating,
# TODO create setting for default ordering - most cooked, rating,
# TODO create options for live sorting # TODO create options for live sorting
# TODO make days of new recipe a setting
if search_new == 'true': if search_new == 'true':
queryset = ( queryset = (
queryset.annotate(new_recipe=Case( queryset.annotate(new_recipe=Case(
When(created_at__gte=(datetime.now() - timedelta(days=81)), then=('pk')), default=Value(0),)) When(created_at__gte=(datetime.now() - timedelta(days=7)), then=('pk')), default=Value(0),))
) )
orderby += ['-new_recipe'] orderby += ['-new_recipe']
@ -167,27 +169,27 @@ def search_recipes(request, queryset, params):
queryset = queryset.order_by("?") queryset = queryset.order_by("?")
else: else:
# TODO add order by user settings # TODO add order by user settings
orderby += ['name'] # orderby += ['name']
queryset = queryset.order_by(*orderby) queryset = queryset.order_by(*orderby)
return queryset return queryset
def get_facet(qs, params, space): def get_facet(qs, request):
# NOTE facet counts for tree models include self AND descendants # NOTE facet counts for tree models include self AND descendants
facets = {} facets = {}
ratings = params.getlist('ratings', []) ratings = request.query_params.getlist('ratings', [])
keyword_list = params.getlist('keywords', []) keyword_list = request.query_params.getlist('keywords', [])
food_list = params.getlist('foods', []) food_list = request.query_params.getlist('foods', [])
book_list = params.getlist('book', []) book_list = request.query_params.getlist('book', [])
search_keywords_or = params.get('keywords_or', True) search_keywords_or = request.query_params.get('keywords_or', True)
search_foods_or = params.get('foods_or', True) search_foods_or = request.query_params.get('foods_or', True)
search_books_or = params.get('books_or', True) search_books_or = request.query_params.get('books_or', True)
# if using an OR search, will annotate all keywords, otherwise, just those that appear in results # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
if search_keywords_or: if search_keywords_or:
keywords = Keyword.objects.filter(space=space).annotate(recipe_count=Count('recipe')) keywords = Keyword.objects.filter(space=request.space).annotate(recipe_count=Count('recipe'))
else: else:
keywords = Keyword.objects.filter(recipe__in=qs, space=space).annotate(recipe_count=Count('recipe')) keywords = Keyword.objects.filter(recipe__in=qs, space=request.space).annotate(recipe_count=Count('recipe'))
# custom django-tree function annotates a queryset to make building a tree easier. # custom django-tree function annotates a queryset to make building a tree easier.
# see https://django-treebeard.readthedocs.io/en/latest/api.html#treebeard.models.Node.get_annotated_list_qs for details # see https://django-treebeard.readthedocs.io/en/latest/api.html#treebeard.models.Node.get_annotated_list_qs for details
kw_a = annotated_qs(keywords, root=True, fill=True) kw_a = annotated_qs(keywords, root=True, fill=True)
@ -199,7 +201,10 @@ def get_facet(qs, params, space):
facets['Foods'] = [] facets['Foods'] = []
# TODO add book facet # TODO add book facet
facets['Books'] = [] facets['Books'] = []
facets['Recent'] = ViewLog.objects.filter(
created_by=request.user, space=request.space,
created_at__gte=datetime.now() - timedelta(days=14) # TODO make days of recent recipe a setting
).values_list('recipe__pk', flat=True)
return facets return facets

View File

@ -745,7 +745,7 @@ class CookLog(ExportModelOperationsMixin('cook_log'), models.Model, PermissionMo
return self.recipe.name return self.recipe.name
class Meta(): class Meta():
indexes = (Index(fields=['id', 'recipe', '-created_at', 'rating']),) indexes = (Index(fields=['id', 'recipe', '-created_at', 'rating', 'created_by']),)
class ViewLog(ExportModelOperationsMixin('view_log'), models.Model, PermissionModelMixin): class ViewLog(ExportModelOperationsMixin('view_log'), models.Model, PermissionModelMixin):
@ -760,7 +760,7 @@ class ViewLog(ExportModelOperationsMixin('view_log'), models.Model, PermissionMo
return self.recipe.name return self.recipe.name
class Meta(): class Meta():
indexes = (Index(fields=['recipe', '-created_at']),) indexes = (Index(fields=['recipe', '-created_at', 'created_by']),)
class ImportLog(models.Model, PermissionModelMixin): class ImportLog(models.Model, PermissionModelMixin):

View File

@ -1,14 +1,14 @@
import random import random
from datetime import timedelta
from decimal import Decimal from decimal import Decimal
from gettext import gettext as _ from gettext import gettext as _
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import QuerySet, Sum, Avg from django.db.models import QuerySet, Sum, Avg
from django.utils import timezone
from drf_writable_nested import (UniqueFieldsMixin, from drf_writable_nested import (UniqueFieldsMixin,
WritableNestedModelSerializer) WritableNestedModelSerializer)
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError, NotFound from rest_framework.exceptions import ValidationError, NotFound
from treebeard.mp_tree import MP_NodeQuerySet
from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword, from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
MealPlan, MealType, NutritionInformation, Recipe, MealPlan, MealType, NutritionInformation, Recipe,
@ -387,11 +387,19 @@ class RecipeBaseSerializer(WritableNestedModelSerializer):
pass pass
return None return None
# TODO make days of new recipe a setting
def is_recipe_new(self, obj):
if obj.created_at > (timezone.now() - timedelta(days=7)):
return True
else:
return False
class RecipeOverviewSerializer(RecipeBaseSerializer): class RecipeOverviewSerializer(RecipeBaseSerializer):
keywords = KeywordLabelSerializer(many=True) keywords = KeywordLabelSerializer(many=True)
rating = serializers.SerializerMethodField('get_recipe_rating') rating = serializers.SerializerMethodField('get_recipe_rating')
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked') last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
new = serializers.SerializerMethodField('is_recipe_new')
def create(self, validated_data): def create(self, validated_data):
pass pass
@ -404,7 +412,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
fields = ( fields = (
'id', 'name', 'description', 'image', 'keywords', 'working_time', 'id', 'name', 'description', 'image', 'keywords', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at', 'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new'
) )
read_only_fields = ['image', 'created_by', 'created_at'] read_only_fields = ['image', 'created_by', 'created_at']

File diff suppressed because one or more lines are too long

View File

@ -475,7 +475,7 @@ class RecipePagination(PageNumberPagination):
max_page_size = 100 max_page_size = 100
def paginate_queryset(self, queryset, request, view=None): def paginate_queryset(self, queryset, request, view=None):
self.facets = get_facet(queryset, request.query_params, request.space) self.facets = get_facet(queryset, request)
return super().paginate_queryset(queryset, request, view) return super().paginate_queryset(queryset, request, view)
def get_paginated_response(self, data): def get_paginated_response(self, data):

View File

@ -228,11 +228,13 @@
:meal_plan="m" :footer_text="m.meal_type_name" :meal_plan="m" :footer_text="m.meal_type_name"
footer_icon="far fa-calendar-alt"></recipe-card> footer_icon="far fa-calendar-alt"></recipe-card>
<recipe-card v-for="r in last_viewed_recipes" v-bind:key="`rv_${r.id}`" :recipe="r" <recipe-card v-for="r in recipes" v-bind:key="`rv_${r.id}`" :recipe="r"
v-bind:footer_text="$t('Recently_Viewed')" footer_icon="fas fa-eye"></recipe-card> :footer_text="isRecentOrNew(r)[0]"
:footer_icon="isRecentOrNew(r)[1]">
</recipe-card>
</template> </template>
<recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r"></recipe-card> <!-- <recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r"></recipe-card> -->
</div> </div>
</div> </div>
</div> </div>
@ -323,20 +325,6 @@ export default {
this.$nextTick(function () { this.$nextTick(function () {
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) { if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
this.settings = Object.assign({}, this.settings, this.$cookies.get(SETTINGS_COOKIE_NAME)) this.settings = Object.assign({}, this.settings, this.$cookies.get(SETTINGS_COOKIE_NAME))
// let cookie_val = this.$cookies.get(SETTINGS_COOKIE_NAME)
// for (let i of Object.keys(cookie_val)) {
// this.$set(this.settings, i, cookie_val[i])
// }
// //TODO i have no idea why the above code does not suffice to update the
// //TODO pagination UI element as $set should update all values reactively but it does not
// setTimeout(function () {
// this.$set(this.settings, 'pagination_page', 0)
// }.bind(this), 50)
// setTimeout(function () {
// this.$set(this.settings, 'pagination_page', cookie_val['pagination_page'])
// }.bind(this), 51)
} }
let urlParams = new URLSearchParams(window.location.search); let urlParams = new URLSearchParams(window.location.search);
@ -354,8 +342,8 @@ export default {
} }
this.loadMealPlan() this.loadMealPlan()
this.loadRecentlyViewed() // this.loadRecentlyViewed()
this.refreshData(false) // this.refreshData(false) // this gets triggered when the cookies get loaded
}) })
this.$i18n.locale = window.CUSTOM_LOCALE this.$i18n.locale = window.CUSTOM_LOCALE
@ -371,7 +359,8 @@ export default {
this.loadMealPlan() this.loadMealPlan()
}, },
'settings.recently_viewed': function () { 'settings.recently_viewed': function () {
this.loadRecentlyViewed() // this.loadRecentlyViewed()
this.refreshData(false)
}, },
'settings.search_input': _debounce(function () { 'settings.search_input': _debounce(function () {
this.settings.pagination_page = 1 this.settings.pagination_page = 1
@ -404,7 +393,8 @@ export default {
random, random,
this.settings.sort_by_new, this.settings.sort_by_new,
this.settings.pagination_page, this.settings.pagination_page,
this.settings.page_count this.settings.page_count,
{query: {last_viewed: this.settings.recently_viewed}}
).then(result => { ).then(result => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.pagination_count = result.data.count this.pagination_count = result.data.count
@ -431,17 +421,18 @@ export default {
this.meal_plans = [] this.meal_plans = []
} }
}, },
loadRecentlyViewed: function () { // DEPRECATED: intergrated into standard FTS queryset
let apiClient = new ApiApiFactory() // loadRecentlyViewed: function () {
if (this.settings.recently_viewed > 0) { // let apiClient = new ApiApiFactory()
apiClient.listRecipes(undefined, undefined, undefined, undefined, undefined, undefined, // if (this.settings.recently_viewed > 0) {
undefined, undefined, undefined, this.settings.sort_by_new, 1, this.settings.recently_viewed, {query: {last_viewed: this.settings.recently_viewed}}).then(result => { // apiClient.listRecipes(undefined, undefined, undefined, undefined, undefined, undefined,
this.last_viewed_recipes = result.data.results // undefined, undefined, undefined, this.settings.sort_by_new, 1, this.settings.recently_viewed, {query: {last_viewed: this.settings.recently_viewed}}).then(result => {
}) // this.last_viewed_recipes = result.data.results
} else { // })
this.last_viewed_recipes = [] // } else {
} // this.last_viewed_recipes = []
}, // }
// },
genericSelectChanged: function (obj) { genericSelectChanged: function (obj) {
this.settings[obj.var] = obj.val this.settings[obj.var] = obj.val
this.refreshData(false) this.refreshData(false)
@ -469,6 +460,17 @@ export default {
children: node.children, children: node.children,
isDefaultExpanded: node.isDefaultExpanded isDefaultExpanded: node.isDefaultExpanded
} }
},
isRecentOrNew: function(x) {
let recent_recipe = [this.$t('Recently_Viewed'), "fas fa-eye"]
let new_recipe = [this.$t('New_Recipe'), "fas fa-splotch"]
if (x.new) {
return new_recipe
} else if (this.facets.Recent.includes(x.id)) {
return recent_recipe
} else {
return [undefined, undefined]
}
} }
} }
} }

View File

@ -1,38 +0,0 @@
<template>
<div>
<b-dropdown variant="link" toggle-class="text-decoration-none" no-caret>
<template #button-content>
<i class="fas fa-ellipsis-v" ></i>
</template>
<b-dropdown-item v-on:click="$emit('item-action', 'edit')" v-if="show_edit">
<i class="fas fa-pencil-alt fa-fw"></i> {{ $t('Edit') }}
</b-dropdown-item>
<b-dropdown-item v-on:click="$emit('item-action', 'delete')" v-if="show_delete">
<i class="fas fa-trash-alt fa-fw"></i> {{ $t('Delete') }}
</b-dropdown-item>
<b-dropdown-item v-on:click="$emit('item-action', 'move')" v-if="show_move">
<i class="fas fa-trash-alt fa-fw"></i> {{ $t('Move') }}
</b-dropdown-item>
<b-dropdown-item v-if="show_merge" v-on:click="$emit('item-action', 'merge')">
<i class="fas fa-trash-alt fa-fw"></i> {{ $t('Merge') }}
</b-dropdown-item>
</b-dropdown>
</div>
</template>
<script>
export default {
name: 'KeywordContextMenu',
props: {
show_edit: {type: Boolean, default: true},
show_delete: {type: Boolean, default: true},
show_move: {type: Boolean, default: false},
show_merge: {type: Boolean, default: false},
}
}
</script>