diff --git a/.idea/recipes.iml b/.idea/recipes.iml
deleted file mode 100644
index 8a0e59c8e..000000000
--- a/.idea/recipes.iml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/cookbook/serializer.py b/cookbook/serializer.py
index 28a348e16..67fd2d54c 100644
--- a/cookbook/serializer.py
+++ b/cookbook/serializer.py
@@ -1,3 +1,4 @@
+import random
import traceback
import uuid
from datetime import datetime, timedelta
@@ -999,6 +1000,17 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
read_only_fields = ('created_by',)
+class AutoMealPlanSerializer(serializers.Serializer):
+
+ start_date = serializers.DateField()
+ end_date = serializers.DateField()
+ meal_type_id = serializers.IntegerField()
+ keywords = KeywordSerializer(many=True)
+ servings = CustomDecimalField()
+ shared = UserSerializer(many=True, required=False, allow_null=True)
+ addshopping = serializers.BooleanField()
+
+
class ShoppingListRecipeSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField('get_name') # should this be done at the front end?
recipe_name = serializers.ReadOnlyField(source='recipe.name')
diff --git a/cookbook/urls.py b/cookbook/urls.py
index 7b6d18380..0308a3958 100644
--- a/cookbook/urls.py
+++ b/cookbook/urls.py
@@ -36,6 +36,7 @@ router.register(r'ingredient', api.IngredientViewSet)
router.register(r'invite-link', api.InviteLinkViewSet)
router.register(r'keyword', api.KeywordViewSet)
router.register(r'meal-plan', api.MealPlanViewSet)
+router.register(r'auto-plan', api.AutoPlanViewSet, basename='auto-plan')
router.register(r'meal-type', api.MealTypeViewSet)
router.register(r'recipe', api.RecipeViewSet)
router.register(r'recipe-book', api.RecipeBookViewSet)
diff --git a/cookbook/views/api.py b/cookbook/views/api.py
index 9f9f64204..96e0ce2d6 100644
--- a/cookbook/views/api.py
+++ b/cookbook/views/api.py
@@ -1,7 +1,9 @@
+import datetime
import io
import json
import mimetypes
import pathlib
+import random
import re
import threading
import traceback
@@ -25,6 +27,7 @@ from django.core.files import File
from django.db.models import Case, Count, Exists, OuterRef, ProtectedError, Q, Subquery, Value, When, Avg, Max
from django.db.models.fields.related import ForeignObjectRel
from django.db.models.functions import Coalesce, Lower
+from django.db.models.signals import post_save
from django.http import FileResponse, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
@@ -94,7 +97,8 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSeri
SyncLogSerializer, SyncSerializer, UnitSerializer,
UserFileSerializer, UserSerializer, UserPreferenceSerializer,
UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer,
- RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer, PropertySerializer)
+ RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer,
+ PropertySerializer, AutoMealPlanSerializer)
from cookbook.views.import_export import get_integration
from recipes import settings
@@ -666,6 +670,67 @@ class MealPlanViewSet(viewsets.ModelViewSet):
return queryset
+class AutoPlanViewSet(viewsets.ViewSet):
+ def create(self, request):
+ serializer = AutoMealPlanSerializer(data=request.data)
+
+ if serializer.is_valid():
+ keywords = serializer.validated_data['keywords']
+ start_date = serializer.validated_data['start_date']
+ end_date = serializer.validated_data['end_date']
+ meal_type = MealType.objects.get(pk=serializer.validated_data['meal_type_id'])
+ servings = serializer.validated_data['servings']
+ shared = serializer.get_initial().get('shared', None)
+ shared_pks = list()
+ if shared is not None:
+ for i in range(len(shared)):
+ shared_pks.append(shared[i]['id'])
+
+ days = (end_date - start_date).days + 1
+ recipes = Recipe.objects.all()
+ meal_plans = list()
+
+ for keyword in keywords:
+ recipes = recipes.filter(keywords__name=keyword['name'])
+
+ if len(recipes) == 0:
+ return Response(serializer.data)
+ recipes = recipes.order_by('?')[:days]
+ recipes = list(recipes)
+
+ for i in range(0, days):
+ day = start_date + datetime.timedelta(i)
+ recipe = recipes[i % len(recipes)]
+ args = {'recipe': recipe, 'servings': servings, 'title': recipe.name,
+ 'created_by': request.user,
+ 'meal_type': meal_type,
+ 'note': '', 'date': day, 'space': request.space}
+
+ m = MealPlan(**args)
+ meal_plans.append(m)
+
+ MealPlan.objects.bulk_create(meal_plans)
+
+ for m in meal_plans:
+ m.shared.set(shared_pks)
+
+ if request.data.get('addshopping', False):
+ SLR = RecipeShoppingEditor(user=request.user, space=request.space)
+ SLR.create(mealplan=m, servings=servings)
+
+ else:
+ post_save.send(
+ sender=m.__class__,
+ instance=m,
+ created=True,
+ update_fields=None,
+ )
+
+ return Response(serializer.data)
+
+ return Response(serializer.errors, 400)
+
+
class MealTypeViewSet(viewsets.ModelViewSet):
"""
returns list of meal types created by the
diff --git a/vue/src/apps/MealPlanView/MealPlanView.vue b/vue/src/apps/MealPlanView/MealPlanView.vue
index 6ed34700d..2763ac0d6 100644
--- a/vue/src/apps/MealPlanView/MealPlanView.vue
+++ b/vue/src/apps/MealPlanView/MealPlanView.vue
@@ -279,12 +279,26 @@
:create_date="mealplan_default_date"
@reload-meal-types="refreshMealTypes"
>
+
@@ -322,6 +337,8 @@ import {CalendarView, CalendarMathMixin} from "vue-simple-calendar/src/component
import {ApiApiFactory} from "@/utils/openapi/api"
import BottomNavigationBar from "@/components/BottomNavigationBar.vue";
import {useMealPlanStore} from "@/stores/MealPlanStore";
+import axios from "axios";
+import AutoMealPlanModal from "@/components/AutoMealPlanModal";
const {makeToast} = require("@/utils/utils")
@@ -334,6 +351,7 @@ let SETTINGS_COOKIE_NAME = "mealplan_settings"
export default {
name: "MealPlanView",
components: {
+ AutoMealPlanModal,
MealPlanEditModal,
MealPlanCard,
CalendarView,
@@ -347,6 +365,16 @@ export default {
mixins: [CalendarMathMixin, ApiMixin, ResolveUrlMixin],
data: function () {
return {
+ AutoPlan: {
+ meal_types: [],
+ keywords: [[]],
+ servings: 1,
+ date: Date.now(),
+ startDay: null,
+ endDay: null,
+ shared: [],
+ addshopping: false
+ },
showDate: new Date(),
plan_entries: [],
recipe_viewed: {},
@@ -656,6 +684,39 @@ export default {
this.$bvModal.show(`id_meal_plan_edit_modal`)
})
+ },
+ createAutoPlan() {
+ this.$bvModal.show(`autoplan-modal`)
+ },
+ async autoPlanThread(autoPlan, mealTypeIndex) {
+ let apiClient = new ApiApiFactory()
+ let data = {
+ "start_date" : moment(autoPlan.startDay).format("YYYY-MM-DD"),
+ "end_date" : moment(autoPlan.endDay).format("YYYY-MM-DD"),
+ "meal_type_id" : autoPlan.meal_types[mealTypeIndex].id,
+ "keywords" : autoPlan.keywords[mealTypeIndex],
+ "servings" : autoPlan.servings,
+ "shared" : autoPlan.shared,
+ "addshopping": autoPlan.addshopping
+ }
+ await apiClient.createAutoPlanViewSet(data)
+
+ },
+ async doAutoPlan(autoPlan) {
+ for (let i = 0; i < autoPlan.meal_types.length; i++) {
+ if (autoPlan.keywords[i].length === 0) continue
+ await this.autoPlanThread(autoPlan, i)
+ }
+ this.refreshEntries()
+ },
+ refreshEntries(){//todo Remove method
+ let date = this.current_period
+ useMealPlanStore().refreshFromAPI(moment(date.periodStart).format("YYYY-MM-DD"), moment(date.periodEnd).format("YYYY-MM-DD"))
+ },
+ deleteAll(){//todo Remove method, only used in debugging
+ for (let i = 0; i < useMealPlanStore().plan_list.length; i++) {
+ useMealPlanStore().deleteObject(useMealPlanStore().plan_list[i])
+ }
}
},
directives: {
diff --git a/vue/src/components/AutoMealPlanModal.vue b/vue/src/components/AutoMealPlanModal.vue
new file mode 100644
index 000000000..a2997c561
--- /dev/null
+++ b/vue/src/components/AutoMealPlanModal.vue
@@ -0,0 +1,219 @@
+
+
+ {{ $t("Meal_Types") }}
+
+
+
+
+
+
+
+ {{ meal_type.icon }} {{
+ meal_type.name
+ }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("Servings") }}
+
+
+
+ {{ $t("Share") }}
+
+
+
+ {{
+ $t("AddToShopping")
+ }}
+
+
+
+
+
+
{{ $t("Start Day") }}
+
+
{{ $t("End Day") }}
+
+
+
+
+
+
+ {{ $t("Create Meal Plan") }}
+ {{ $t("Exit") }}
+
+
+
+
+
+
+
+
diff --git a/vue/src/utils/openapi/api.ts b/vue/src/utils/openapi/api.ts
index 47bca8b10..f180b5469 100644
--- a/vue/src/utils/openapi/api.ts
+++ b/vue/src/utils/openapi/api.ts
@@ -5356,6 +5356,39 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions,
};
},
+ /**
+ *
+ * @param {any} [body]
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ createAutoPlanViewSet: async (body?: any, options: any = {}): Promise
=> {
+ const localVarPath = `/api/auto-plan/`;
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+
+
+ localVarHeaderParameter['Content-Type'] = 'application/json';
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+ localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
/**
*
* @param {Automation} [automation]
@@ -14686,6 +14719,16 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.createAccessToken(accessToken, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
+ /**
+ *
+ * @param {any} [body]
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async createAutoPlanViewSet(body?: any, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.createAutoPlanViewSet(body, options);
+ return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+ },
/**
*
* @param {Automation} [automation]
@@ -17464,6 +17507,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
createAccessToken(accessToken?: AccessToken, options?: any): AxiosPromise {
return localVarFp.createAccessToken(accessToken, options).then((request) => request(axios, basePath));
},
+ /**
+ *
+ * @param {any} [body]
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ createAutoPlanViewSet(body?: any, options?: any): AxiosPromise {
+ return localVarFp.createAutoPlanViewSet(body, options).then((request) => request(axios, basePath));
+ },
/**
*
* @param {Automation} [automation]
@@ -19981,6 +20033,17 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).createAccessToken(accessToken, options).then((request) => request(this.axios, this.basePath));
}
+ /**
+ *
+ * @param {any} [body]
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof ApiApi
+ */
+ public createAutoPlanViewSet(body?: any, options?: any) {
+ return ApiApiFp(this.configuration).createAutoPlanViewSet(body, options).then((request) => request(this.axios, this.basePath));
+ }
+
/**
*
* @param {Automation} [automation]