further improvements to search view

This commit is contained in:
vabene1111 2021-06-28 18:35:20 +02:00
parent 78885987f0
commit 272341f1dc
14 changed files with 208 additions and 52 deletions

View File

@ -2,7 +2,7 @@ from decimal import Decimal
from gettext import gettext as _
from django.contrib.auth.models import User
from django.db.models import QuerySet, Sum
from django.db.models import QuerySet, Sum, Avg
from drf_writable_nested import (UniqueFieldsMixin,
WritableNestedModelSerializer)
from rest_framework import serializers
@ -330,8 +330,24 @@ class NutritionInformationSerializer(serializers.ModelSerializer):
fields = ('id', 'carbohydrates', 'fats', 'proteins', 'calories', 'source')
class RecipeOverviewSerializer(WritableNestedModelSerializer):
class RecipeBaseSerializer(WritableNestedModelSerializer):
def get_recipe_rating(self, obj):
rating = obj.cooklog_set.filter(created_by=self.context['request'].user, rating__gt=0).aggregate(Avg('rating'))
if rating['rating__avg']:
return rating['rating__avg']
return 0
def get_recipe_last_cooked(self, obj):
last = obj.cooklog_set.filter(created_by=self.context['request'].user).last()
if last:
return last.created_at
return None
class RecipeOverviewSerializer(RecipeBaseSerializer):
keywords = KeywordLabelSerializer(many=True)
rating = serializers.SerializerMethodField('get_recipe_rating')
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
def create(self, validated_data):
pass
@ -344,22 +360,24 @@ class RecipeOverviewSerializer(WritableNestedModelSerializer):
fields = (
'id', 'name', 'description', 'image', 'keywords', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'servings', 'file_path'
'internal', 'servings', 'servings_text', 'rating', 'last_cooked',
)
read_only_fields = ['image', 'created_by', 'created_at']
class RecipeSerializer(WritableNestedModelSerializer):
class RecipeSerializer(RecipeBaseSerializer):
nutrition = NutritionInformationSerializer(allow_null=True, required=False)
steps = StepSerializer(many=True)
keywords = KeywordSerializer(many=True)
rating = serializers.SerializerMethodField('get_recipe_rating')
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
class Meta:
model = Recipe
fields = (
'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'nutrition', 'servings', 'file_path', 'servings_text',
'internal', 'nutrition', 'servings', 'file_path', 'servings_text', 'rating', 'last_cooked',
)
read_only_fields = ['image', 'created_by', 'created_at']

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -15,7 +15,9 @@
<b-input-group class="mt-3">
<b-input class="form-control" v-model="settings.search_input" v-bind:placeholder="$t('Search')"></b-input>
<b-input-group-append>
<b-button v-b-toggle.collapse_advanced_search v-bind:class="{'btn-primary': !isAdvancedSettingsSet(), 'btn-danger': isAdvancedSettingsSet()}" class="shadow-none btn"><i
<b-button v-b-toggle.collapse_advanced_search
v-bind:class="{'btn-primary': !isAdvancedSettingsSet(), 'btn-danger': isAdvancedSettingsSet()}"
class="shadow-none btn"><i
class="fas fa-caret-down" v-if="!settings.advanced_search_visible"></i><i class="fas fa-caret-up"
v-if="settings.advanced_search_visible"></i>
</b-button>
@ -37,21 +39,16 @@
:href="resolveDjangoUrl('data_import_url')">{{ $t('Import') }}</a>
</div>
<div class="col-md-3" style="margin-top: 1vh">
<button class="btn btn-primary btn-block text-uppercase" @click="resetSearch">
{{ $t('Reset_Search') }}
<button class="btn btn-block text-uppercase" v-b-tooltip.hover :title="$t('show_only_internal')"
v-bind:class="{'btn-success':settings.search_internal, 'btn-primary':!settings.search_internal}"
@click="settings.search_internal = !settings.search_internal;refreshData()">
{{ $t('Internal') }}
</button>
</div>
<div class="col-md-2" style="position: relative; margin-top: 1vh">
<b-form-checkbox v-model="settings.search_internal" name="check-button"
@change="refreshData(false)"
class="shadow-none"
style="position:relative;top: 50%; transform: translateY(-50%);" switch>
{{ $t('show_only_internal') }}
</b-form-checkbox>
</div>
<div class="col-md-1" style="position: relative; margin-top: 1vh">
<button id="id_settings_button" class="btn btn-primary btn-block"><i class="fas fa-cog"></i>
<div class="col-md-3" style="position: relative; margin-top: 1vh">
<button id="id_settings_button" class="btn btn-primary btn-block text-uppercase"><i
class="fas fa-cog"></i>
</button>
</div>
@ -175,7 +172,16 @@
</div>
</div>
<div class="row" style="margin-top: 2vh">
<div class="row">
<div class="col col-md-12 text-right" style="margin-top: 2vh">
<span class="text-muted">
{{ $t('Page') }} {{ settings.pagination_page }}/{{ pagination_count }} <a href="#" @click="resetSearch"><i
class="fas fa-times-circle"></i> {{ $t('Reset') }}</a>
</span>
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;">
@ -194,10 +200,16 @@
</div>
</div>
<div class="row" style="margin-top: 2vh; text-align: center">
<div class="row" style="margin-top: 2vh">
<div class="col col-md-12">
<b-button @click="loadMore()" class="btn-block btn-success" v-if="pagination_more">{{ $t('Load_More') }}
</b-button>
<b-pagination pills
v-model="settings.pagination_page"
:total-rows="pagination_count"
per-page="25"
@change="pageChange"
align="center">
</b-pagination>
</div>
</div>
@ -233,6 +245,8 @@ import GenericMultiselect from "@/components/GenericMultiselect";
Vue.use(BootstrapVue)
let SETTINGS_COOKIE_NAME = 'search_settings'
export default {
name: 'RecipeSearchView',
mixins: [ResolveUrlMixin],
@ -243,6 +257,7 @@ export default {
meal_plans: [],
last_viewed_recipes: [],
settings_loaded: false,
settings: {
search_input: '',
search_internal: false,
@ -256,19 +271,33 @@ export default {
advanced_search_visible: false,
show_meal_plan: true,
recently_viewed: 5,
pagination_page: 1,
},
pagination_more: true,
pagination_page: 1,
pagination_count: 0,
}
},
mounted() {
this.$nextTick(function () {
if (this.$cookies.isKey('search_settings_v2')) {
this.settings = this.$cookies.get("search_settings_v2")
if (this.$cookies.isKey(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 apiClient = new ApiApiFactory()
@ -278,7 +307,7 @@ export default {
let keyword = {id: x, name: 'loading'}
this.settings.search_keywords.push(keyword)
apiClient.retrieveKeyword(x).then(result => {
this.$set(this.settings.search_keywords,this.settings.search_keywords.indexOf(keyword), result.data)
this.$set(this.settings.search_keywords, this.settings.search_keywords.indexOf(keyword), result.data)
})
}
}
@ -293,7 +322,7 @@ export default {
watch: {
settings: {
handler() {
this.$cookies.set("search_settings_v2", this.settings, -1)
this.$cookies.set(SETTINGS_COOKIE_NAME, this.settings, -1)
},
deep: true
},
@ -327,16 +356,11 @@ export default {
this.settings.search_internal,
undefined,
this.pagination_page,
this.settings.pagination_page,
).then(result => {
this.pagination_more = (result.data.next !== null)
if (page_load) {
for (let x of result.data.results) {
this.recipes.push(x)
}
} else {
this.recipes = result.data.results
}
window.scrollTo(0, 0);
this.pagination_count = result.data.count
this.recipes = result.data.results
})
},
loadMealPlan: function () {
@ -378,13 +402,14 @@ export default {
this.settings.search_keywords = []
this.settings.search_foods = []
this.settings.search_books = []
this.settings.pagination_page = 1
this.refreshData(false)
},
loadMore: function (page) {
this.pagination_page++
this.refreshData(true)
pageChange: function (page) {
this.settings.pagination_page = page
this.refreshData()
},
isAdvancedSettingsSet(){
isAdvancedSettingsSet() {
return ((this.settings.search_keywords.length + this.settings.search_foods.length + this.settings.search_books.length) > 0)
}
}

View File

@ -11,6 +11,13 @@
</div>
</div>
<div class="row text-center">
<div class="col col-md-12">
<recipe-rating :recipe="recipe"></recipe-rating> <br/>
<last-cooked :recipe="recipe"></last-cooked>
</div>
</div>
<div class="my-auto">
<div class="col-12" style="text-align: center">
<i>{{ recipe.description }}</i>
@ -166,6 +173,8 @@ import moment from 'moment'
import Keywords from "@/components/Keywords";
import LoadingSpinner from "@/components/LoadingSpinner";
import AddRecipeToBook from "@/components/AddRecipeToBook";
import RecipeRating from "@/components/RecipeRating";
import LastCooked from "@/components/LastCooked";
Vue.prototype.moment = moment
@ -178,6 +187,8 @@ export default {
ToastMixin,
],
components: {
LastCooked,
RecipeRating,
PdfViewer,
ImageViewer,
Ingredient,

View File

@ -0,0 +1,28 @@
<template>
<span>
<b-badge pill variant="primary" v-if="recipe.last_cooked !== null"><i class="fas fa-utensils"></i> {{
formatDate(recipe.last_cooked)
}}</b-badge>
</span>
</template>
<script>
import moment from "moment/moment";
export default {
name: "LastCooked",
props: {
recipe: Object
},
methods: {
formatDate: function (datetime) {
moment.locale(window.navigator.language);
return moment(datetime).format('L')
}
}
}
</script>
<style scoped>
</style>

View File

@ -7,7 +7,9 @@
top></b-card-img-lazy>
<div class="card-img-overlay h-100 d-flex flex-column justify-content-right"
style="float:right; text-align: right; padding-top: 10px; padding-right: 5px">
<a><recipe-context-menu :recipe="recipe" style="float:right" v-if="recipe !== null"></recipe-context-menu></a>
<a>
<recipe-context-menu :recipe="recipe" style="float:right" v-if="recipe !== null"></recipe-context-menu>
</a>
</div>
</a>
@ -21,12 +23,18 @@
<b-card-text style="text-overflow: ellipsis;">
<template v-if="recipe !== null">
{{ recipe.description }}
<recipe-rating :recipe="recipe"></recipe-rating> <br/> <!-- TODO UGLY! -->
<last-cooked :recipe="recipe"></last-cooked>
<keywords :recipe="recipe" style="margin-top: 4px"></keywords>
<b-badge pill variant="info" v-if="!recipe.internal">{{ $t('External') }}</b-badge>
<b-badge pill variant="success"
v-if="Date.parse(recipe.created_at) > new Date(Date.now() - (7 * (1000 * 60 * 60 * 24)))">
{{ $t('New') }}
</b-badge>
</template>
<template v-else>{{ meal_plan.note }}</template>
</b-card-text>
@ -44,13 +52,19 @@
import RecipeContextMenu from "@/components/RecipeContextMenu";
import Keywords from "@/components/Keywords";
import {resolveDjangoUrl, ResolveUrlMixin} from "@/utils/utils";
import RecipeRating from "@/components/RecipeRating";
import moment from "moment/moment";
import Vue from "vue";
import LastCooked from "@/components/LastCooked";
Vue.prototype.moment = moment
export default {
name: "RecipeCard",
mixins: [
ResolveUrlMixin,
],
components: {Keywords, RecipeContextMenu},
components: {LastCooked, RecipeRating, Keywords, RecipeContextMenu},
props: {
recipe: Object,
meal_plan: Object,

View File

@ -0,0 +1,24 @@
<template>
<div>
<span style="display: inline-block;" v-if="recipe.rating > 0">
<i class="fas fa-star fa-xs" v-for="i in Math.floor(recipe.rating)" v-bind:key="i"></i>
<i class="fas fa-star-half-alt fa-xs" v-if="recipe.rating % 1 > 0"></i>
</span>
</div>
</template>
<script>
export default {
name: "RecipeRating",
props: {
recipe: Object
}
}
</script>
<style scoped>
</style>

View File

@ -564,7 +564,19 @@ export interface MealPlanRecipe {
* @type {string}
* @memberof MealPlanRecipe
*/
file_path?: string;
servings_text?: string;
/**
*
* @type {string}
* @memberof MealPlanRecipe
*/
rating?: string;
/**
*
* @type {string}
* @memberof MealPlanRecipe
*/
last_cooked?: string;
}
/**
*
@ -699,6 +711,18 @@ export interface Recipe {
* @memberof Recipe
*/
servings_text?: string;
/**
*
* @type {string}
* @memberof Recipe
*/
rating?: string;
/**
*
* @type {string}
* @memberof Recipe
*/
last_cooked?: string;
}
/**
*
@ -956,7 +980,19 @@ export interface RecipeOverview {
* @type {string}
* @memberof RecipeOverview
*/
file_path?: string;
servings_text?: string;
/**
*
* @type {string}
* @memberof RecipeOverview
*/
rating?: string;
/**
*
* @type {string}
* @memberof RecipeOverview
*/
last_cooked?: string;
}
/**
*