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 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
@ -30,26 +30,28 @@ def search_recipes(request, queryset, params):
search_random = params.get('random', False)
search_new = params.get('new', False)
search_last_viewed = int(params.get('last_viewed', 0))
orderby = []
# TODO update this to concat with full search queryset qs1 | qs2
if search_last_viewed > 0:
last_viewed_recipes = ViewLog.objects.filter(
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)
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)
# 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 make days of new recipe a setting
if search_new == 'true':
queryset = (
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']
@ -167,27 +169,27 @@ def search_recipes(request, queryset, params):
queryset = queryset.order_by("?")
else:
# TODO add order by user settings
orderby += ['name']
# orderby += ['name']
queryset = queryset.order_by(*orderby)
return queryset
def get_facet(qs, params, space):
def get_facet(qs, request):
# NOTE facet counts for tree models include self AND descendants
facets = {}
ratings = params.getlist('ratings', [])
keyword_list = params.getlist('keywords', [])
food_list = params.getlist('foods', [])
book_list = params.getlist('book', [])
search_keywords_or = params.get('keywords_or', True)
search_foods_or = params.get('foods_or', True)
search_books_or = params.get('books_or', True)
ratings = request.query_params.getlist('ratings', [])
keyword_list = request.query_params.getlist('keywords', [])
food_list = request.query_params.getlist('foods', [])
book_list = request.query_params.getlist('book', [])
search_keywords_or = request.query_params.get('keywords_or', True)
search_foods_or = request.query_params.get('foods_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 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:
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.
# 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)
@ -199,7 +201,10 @@ def get_facet(qs, params, space):
facets['Foods'] = []
# TODO add book facet
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

View File

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

View File

@ -1,14 +1,14 @@
import random
from datetime import timedelta
from decimal import Decimal
from gettext import gettext as _
from django.contrib.auth.models import User
from django.db.models import QuerySet, Sum, Avg
from django.utils import timezone
from drf_writable_nested import (UniqueFieldsMixin,
WritableNestedModelSerializer)
from rest_framework import serializers
from rest_framework.exceptions import ValidationError, NotFound
from treebeard.mp_tree import MP_NodeQuerySet
from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
MealPlan, MealType, NutritionInformation, Recipe,
@ -387,11 +387,19 @@ class RecipeBaseSerializer(WritableNestedModelSerializer):
pass
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):
keywords = KeywordLabelSerializer(many=True)
rating = serializers.SerializerMethodField('get_recipe_rating')
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
new = serializers.SerializerMethodField('is_recipe_new')
def create(self, validated_data):
pass
@ -404,7 +412,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
fields = (
'id', 'name', 'description', 'image', 'keywords', 'working_time',
'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']

File diff suppressed because one or more lines are too long

View File

@ -475,7 +475,7 @@ class RecipePagination(PageNumberPagination):
max_page_size = 100
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)
def get_paginated_response(self, data):

View File

@ -228,11 +228,13 @@
:meal_plan="m" :footer_text="m.meal_type_name"
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"
v-bind:footer_text="$t('Recently_Viewed')" footer_icon="fas fa-eye"></recipe-card>
<recipe-card v-for="r in recipes" v-bind:key="`rv_${r.id}`" :recipe="r"
:footer_text="isRecentOrNew(r)[0]"
:footer_icon="isRecentOrNew(r)[1]">
</recipe-card>
</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>
@ -323,20 +325,6 @@ export default {
this.$nextTick(function () {
if (this.$cookies.isKey(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);
@ -354,8 +342,8 @@ export default {
}
this.loadMealPlan()
this.loadRecentlyViewed()
this.refreshData(false)
// this.loadRecentlyViewed()
// this.refreshData(false) // this gets triggered when the cookies get loaded
})
this.$i18n.locale = window.CUSTOM_LOCALE
@ -371,7 +359,8 @@ export default {
this.loadMealPlan()
},
'settings.recently_viewed': function () {
this.loadRecentlyViewed()
// this.loadRecentlyViewed()
this.refreshData(false)
},
'settings.search_input': _debounce(function () {
this.settings.pagination_page = 1
@ -404,7 +393,8 @@ export default {
random,
this.settings.sort_by_new,
this.settings.pagination_page,
this.settings.page_count
this.settings.page_count,
{query: {last_viewed: this.settings.recently_viewed}}
).then(result => {
window.scrollTo(0, 0);
this.pagination_count = result.data.count
@ -431,17 +421,18 @@ export default {
this.meal_plans = []
}
},
loadRecentlyViewed: function () {
let apiClient = new ApiApiFactory()
if (this.settings.recently_viewed > 0) {
apiClient.listRecipes(undefined, undefined, undefined, undefined, undefined, undefined,
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 = []
}
},
// DEPRECATED: intergrated into standard FTS queryset
// loadRecentlyViewed: function () {
// let apiClient = new ApiApiFactory()
// if (this.settings.recently_viewed > 0) {
// apiClient.listRecipes(undefined, undefined, undefined, undefined, undefined, undefined,
// 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 = []
// }
// },
genericSelectChanged: function (obj) {
this.settings[obj.var] = obj.val
this.refreshData(false)
@ -469,6 +460,17 @@ export default {
children: node.children,
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>