consolidated recently viewed into FTS queryset
This commit is contained in:
@ -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 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
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
@ -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):
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
Reference in New Issue
Block a user