Merge pull request #2808 from smilerz/add_mealtype_filter

add ability to filter meal plans based on type
This commit is contained in:
vabene1111 2023-12-20 15:55:09 +01:00 committed by GitHub
commit 4de9be5c89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 134 additions and 3855 deletions

View File

@ -61,6 +61,12 @@ def test_list_filter(obj_1, u1_s1):
response = json.loads(r.content) response = json.loads(r.content)
assert len(response) == 1 assert len(response) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?meal_type={response[0]["meal_type"]["id"]}').content)
assert len(response) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?meal_type=0').content)
assert len(response) == 0
response = json.loads( response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?from_date={(datetime.now() + timedelta(days=2)).strftime("%Y-%m-%d")}').content) u1_s1.get(f'{reverse(LIST_URL)}?from_date={(datetime.now() + timedelta(days=2)).strftime("%Y-%m-%d")}').content)
assert len(response) == 0 assert len(response) == 0

View File

@ -26,7 +26,7 @@ from django.db.models import Case, Count, Exists, OuterRef, ProtectedError, Q, S
from django.db.models.fields.related import ForeignObjectRel from django.db.models.fields.related import ForeignObjectRel
from django.db.models.functions import Coalesce, Lower from django.db.models.functions import Coalesce, Lower
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.http import FileResponse, HttpResponse, JsonResponse, HttpResponseRedirect from django.http import FileResponse, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@ -70,12 +70,13 @@ from cookbook.helper.recipe_url_import import (clean_dict, get_from_youtube_scra
from cookbook.helper.scrapers.scrapers import text_scraper from cookbook.helper.scrapers.scrapers import text_scraper
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food, from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food,
FoodInheritField, ImportLog, Ingredient, InviteLink, Keyword, MealPlan, FoodInheritField, FoodProperty, ImportLog, Ingredient, InviteLink,
MealType, Property, PropertyType, Recipe, RecipeBook, RecipeBookEntry, Keyword, MealPlan, MealType, Property, PropertyType, Recipe,
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList,
Step, Storage, Supermarket, SupermarketCategory, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
UserFile, UserPreference, UserSpace, ViewLog, FoodProperty) SyncLog, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
ViewLog)
from cookbook.provider.dropbox import Dropbox from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud from cookbook.provider.nextcloud import Nextcloud
@ -640,9 +641,9 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
Property.objects.filter(space=self.request.space, import_food_id=food.id).update(import_food_id=None) Property.objects.filter(space=self.request.space, import_food_id=food.id).update(import_food_id=None)
return self.retrieve(request, pk) return self.retrieve(request, pk)
except Exception as e: except Exception:
traceback.print_exc() traceback.print_exc()
return JsonResponse({'msg': f'there was an error parsing the FDC data, please check the server logs'}, status=500, json_dumps_params={'indent': 4}) return JsonResponse({'msg': 'there was an error parsing the FDC data, please check the server logs'}, status=500, json_dumps_params={'indent': 4})
def destroy(self, *args, **kwargs): def destroy(self, *args, **kwargs):
try: try:
@ -698,11 +699,18 @@ class MealPlanViewSet(viewsets.ModelViewSet):
- **from_date**: filter from (inclusive) a certain date onward - **from_date**: filter from (inclusive) a certain date onward
- **to_date**: filter upward to (inclusive) certain date - **to_date**: filter upward to (inclusive) certain date
- **meal_type**: filter meal plans based on meal_type ID
""" """
queryset = MealPlan.objects queryset = MealPlan.objects
serializer_class = MealPlanSerializer serializer_class = MealPlanSerializer
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope] permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
query_params = [
QueryParam(name='from_date', description=_('Filter meal plans from date (inclusive) in the format of YYYY-MM-DD.'), qtype='string'),
QueryParam(name='to_date', description=_('Filter meal plans to date (inclusive) in the format of YYYY-MM-DD.'), qtype='string'),
QueryParam(name='meal_type', description=_('Filter meal plans with MealType ID. For multiple repeat parameter.'), qtype='int'),
]
schema = QueryParamAutoSchema()
def get_queryset(self): def get_queryset(self):
queryset = self.queryset.filter( queryset = self.queryset.filter(
@ -717,6 +725,11 @@ class MealPlanViewSet(viewsets.ModelViewSet):
to_date = self.request.query_params.get('to_date', None) to_date = self.request.query_params.get('to_date', None)
if to_date is not None: if to_date is not None:
queryset = queryset.filter(to_date__lte=to_date) queryset = queryset.filter(to_date__lte=to_date)
meal_type = self.request.query_params.getlist('meal_type', [])
if meal_type:
queryset = queryset.filter(meal_type__in=meal_type)
return queryset return queryset

View File

@ -3,8 +3,7 @@
<div id="switcher" class="align-center"> <div id="switcher" class="align-center">
<i class="btn btn-primary fas fa-receipt fa-xl fa-fw shadow-none btn-circle" v-b-toggle.related-recipes /> <i class="btn btn-primary fas fa-receipt fa-xl fa-fw shadow-none btn-circle" v-b-toggle.related-recipes />
</div> </div>
<b-sidebar id="related-recipes" backdrop right bottom no-header shadow="sm" style="z-index: 10000" <b-sidebar id="related-recipes" backdrop right bottom no-header shadow="sm" style="z-index: 10000" @shown="updatePinnedRecipes()">
@shown="updatePinnedRecipes()">
<template #default="{ hide }"> <template #default="{ hide }">
<div class="d-flex flex-column justify-content-end h-100 p-3 align-items-end"> <div class="d-flex flex-column justify-content-end h-100 p-3 align-items-end">
<h5>{{ $t("Planned") }} <i class="fas fa-calendar fa-fw"></i></h5> <h5>{{ $t("Planned") }} <i class="fas fa-calendar fa-fw"></i></h5>
@ -36,8 +35,7 @@
<div v-for="r in pinned_recipes" :key="`pin${r.id}`"> <div v-for="r in pinned_recipes" :key="`pin${r.id}`">
<b-row class="pb-1 pt-1"> <b-row class="pb-1 pt-1">
<b-col cols="2"> <b-col cols="2">
<a href="javascript:void(0)" @click="unPinRecipe(r)" class="text-muted"><i <a href="javascript:void(0)" @click="unPinRecipe(r)" class="text-muted"><i class="fas fa-times"></i></a>
class="fas fa-times"></i></a>
</b-col> </b-col>
<b-col cols="10"> <b-col cols="10">
<a <a
@ -160,12 +158,14 @@ export default {
// get related recipes and save them for later // get related recipes and save them for later
if (this.$parent.recipe) { if (this.$parent.recipe) {
this.related_recipes = [this.$parent.recipe] this.related_recipes = [this.$parent.recipe]
return apiClient.relatedRecipe(this.$parent.recipe.id, { return apiClient
.relatedRecipe(this.$parent.recipe.id, {
query: { query: {
levels: 2, levels: 2,
format: "json" format: "json",
} },
}).then((result) => { })
.then((result) => {
this.related_recipes = this.related_recipes.concat(result.data) this.related_recipes = this.related_recipes.concat(result.data)
}) })
} }
@ -179,7 +179,7 @@ export default {
// TODO move to utility function moment is in maintenance mode https://momentjs.com/docs/ // TODO move to utility function moment is in maintenance mode https://momentjs.com/docs/
var tzoffset = new Date().getTimezoneOffset() * 60000 //offset in milliseconds var tzoffset = new Date().getTimezoneOffset() * 60000 //offset in milliseconds
let today = new Date(Date.now() - tzoffset).toISOString().split("T")[0] let today = new Date(Date.now() - tzoffset).toISOString().split("T")[0]
return apiClient.listMealPlans({query: {from_date: today, to_date: today}}).then((result) => { return apiClient.listMealPlans(today, today).then((result) => {
let promises = [] let promises = []
result.data.forEach((mealplan) => { result.data.forEach((mealplan) => {
this.planned_recipes.push({ ...mealplan?.recipe, servings: mealplan?.servings }) this.planned_recipes.push({ ...mealplan?.recipe, servings: mealplan?.servings })
@ -220,7 +220,6 @@ export default {
z-index: 9000; z-index: 9000;
} }
@media (max-width: 991.98px) { @media (max-width: 991.98px) {
#switcher .btn-circle { #switcher .btn-circle {
position: fixed; position: fixed;

View File

@ -1,10 +1,10 @@
import {defineStore} from 'pinia' import { ApiApiFactory } from "@/utils/openapi/api"
import {ApiApiFactory} from "@/utils/openapi/api"; import { StandardToasts } from "@/utils/utils"
import { defineStore } from "pinia"
const _STORE_ID = 'meal_plan_store'
const _LOCAL_STORAGE_KEY = 'MEAL_PLAN_CLIENT_SETTINGS'
import Vue from "vue" import Vue from "vue"
import {StandardToasts} from "@/utils/utils";
const _STORE_ID = "meal_plan_store"
const _LOCAL_STORAGE_KEY = "MEAL_PLAN_CLIENT_SETTINGS"
/* /*
* test store to play around with pinia and see if it can work for my usecases * test store to play around with pinia and see if it can work for my usecases
* dont trust that all mealplans are in store as there is no cache validation logic, its just a shared data holder * dont trust that all mealplans are in store as there is no cache validation logic, its just a shared data holder
@ -19,7 +19,7 @@ export const useMealPlanStore = defineStore(_STORE_ID, {
plan_list: function () { plan_list: function () {
let plan_list = [] let plan_list = []
for (let key in this.plans) { for (let key in this.plans) {
plan_list.push(this.plans[key]); plan_list.push(this.plans[key])
} }
return plan_list return plan_list
}, },
@ -35,7 +35,7 @@ export const useMealPlanStore = defineStore(_STORE_ID, {
servings: 1, servings: 1,
shared: [], shared: [],
title: "", title: "",
title_placeholder: 'Title', // meal plan edit modal should be improved to not need this title_placeholder: "Title", // meal plan edit modal should be improved to not need this
} }
}, },
client_settings: function () { client_settings: function () {
@ -43,22 +43,15 @@ export const useMealPlanStore = defineStore(_STORE_ID, {
this.settings = this.loadClientSettings() this.settings = this.loadClientSettings()
} }
return this.settings return this.settings
} },
}, },
actions: { actions: {
refreshFromAPI(from_date, to_date) { refreshFromAPI(from_date, to_date) {
if (this.currently_updating !== [from_date, to_date]) { if (this.currently_updating !== [from_date, to_date]) {
this.currently_updating = [from_date, to_date] // certainly no perfect check but better than nothing this.currently_updating = [from_date, to_date] // certainly no perfect check but better than nothing
let options = {
query: {
from_date: from_date,
to_date: to_date,
},
}
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
apiClient.listMealPlans(options).then(r => { apiClient.listMealPlans(from_date, to_date).then((r) => {
r.data.forEach((p) => { r.data.forEach((p) => {
Vue.set(this.plans, p.id, p) Vue.set(this.plans, p.id, p)
}) })
@ -68,29 +61,38 @@ export const useMealPlanStore = defineStore(_STORE_ID, {
}, },
createObject(object) { createObject(object) {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
return apiClient.createMealPlan(object).then(r => { return apiClient
.createMealPlan(object)
.then((r) => {
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE) //StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
Vue.set(this.plans, r.data.id, r.data) Vue.set(this.plans, r.data.id, r.data)
return r return r
}).catch(err => { })
.catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err) StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err)
}) })
}, },
updateObject(object) { updateObject(object) {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
return apiClient.updateMealPlan(object.id, object).then(r => { return apiClient
.updateMealPlan(object.id, object)
.then((r) => {
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE) //StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
Vue.set(this.plans, object.id, object) Vue.set(this.plans, object.id, object)
}).catch(err => { })
.catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err) StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
}) })
}, },
deleteObject(object) { deleteObject(object) {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
return apiClient.destroyMealPlan(object.id).then(r => { return apiClient
.destroyMealPlan(object.id)
.then((r) => {
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_DELETE) //StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_DELETE)
Vue.delete(this.plans, object.id) Vue.delete(this.plans, object.id)
}).catch(err => { })
.catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err) StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
}) })
}, },
@ -110,6 +112,6 @@ export const useMealPlanStore = defineStore(_STORE_ID, {
} else { } else {
return JSON.parse(s) return JSON.parse(s)
} }
} },
}, },
}) })

File diff suppressed because it is too large Load Diff