From b542583303def0108cf7152b2f4cc595cd7c9491 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 12 Sep 2021 11:05:09 -0800 Subject: [PATCH] =?UTF-8?q?=20feat(backend):=20=E2=9C=A8=20rewrite=20mealp?= =?UTF-8?q?lanner=20with=20simple=20api=20(#683)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(backend): :sparkles: new meal-planner feature * feat(frontend): :sparkles: new meal plan feature * refactor(backend): :recycle: refactor base services classes and add mixins for crud * feat(frontend): :sparkles: add UI/API for mealplanner * feat(backend): :sparkles: add get_today and get_slice options for mealplanner * test(backend): :white_check_mark: add and update group mealplanner tests * fix(backend): :bug: Fix recipe_id column type for PG Co-authored-by: hay-kot --- frontend/api/class-interfaces/_base.ts | 17 +- .../{cookbooks.ts => group-cookbooks.ts} | 0 .../api/class-interfaces/group-mealplan.ts | 32 +++ frontend/api/index.ts | 5 +- .../components/Domain/Recipe/RecipeCard.vue | 5 +- .../Domain/Recipe/RecipeCardMobile.vue | 51 ++-- .../Domain/Recipe/RecipeCategoryTagDialog.vue | 2 +- .../Recipe/RecipeSearchFilterSelector.vue | 2 +- frontend/composables/use-group-cookbooks.ts | 2 +- frontend/composables/use-group-mealplan.ts | 80 +++++++ frontend/package.json | 3 +- frontend/pages/meal-plan/planner.vue | 219 ++++++++++++++++-- frontend/pages/meal-plan/this-week.vue | 22 +- frontend/pages/search.vue | 2 +- frontend/plugins/globals.ts | 4 + frontend/tsconfig.json | 2 +- frontend/yarn.lock | 5 + mealie/db/data_access_layer/db_access.py | 9 +- .../db/data_access_layer/meal_access_model.py | 26 +++ mealie/db/init_db.py | 2 +- mealie/db/models/_all_models.py | 1 - mealie/db/models/group/__init__.py | 1 + mealie/db/models/group/group.py | 7 +- mealie/db/models/group/mealplan.py | 24 ++ mealie/db/models/mealplan.py | 82 ------- mealie/db/models/recipe/recipe.py | 4 +- mealie/routes/app/__init__.py | 3 +- mealie/routes/app/app_defaults.py | 12 - mealie/routes/groups/__init__.py | 27 ++- mealie/schema/meal_plan/__init__.py | 1 + mealie/schema/meal_plan/meal.py | 18 -- mealie/schema/meal_plan/new_meal.py | 51 ++++ mealie/schema/user/user.py | 3 +- .../_base_http_service/base_http_service.py | 47 ++-- .../_base_http_service/crud_http_mixins.py | 49 ++++ .../_base_http_service/router_factory.py | 5 +- .../group_services/cookbook_service.py | 12 +- .../services/group_services/group_service.py | 1 - .../{group_mixins.py => group_utils.py} | 2 +- .../services/group_services/meal_service.py | 47 ++++ .../group_services/webhook_service.py | 40 +--- .../user_services/registration_service.py | 2 +- .../user_group_tests/test_group_cookbooks.py | 2 +- .../user_group_tests/test_group_mealplan.py | 167 +++++++++++++ .../user_group_tests/test_group_webhooks.py | 2 +- .../validator_tests/test_create_plan_entry.py | 24 ++ 46 files changed, 869 insertions(+), 255 deletions(-) rename frontend/api/class-interfaces/{cookbooks.ts => group-cookbooks.ts} (100%) create mode 100644 frontend/api/class-interfaces/group-mealplan.ts create mode 100644 frontend/composables/use-group-mealplan.ts create mode 100644 mealie/db/data_access_layer/meal_access_model.py create mode 100644 mealie/db/models/group/mealplan.py delete mode 100644 mealie/db/models/mealplan.py delete mode 100644 mealie/routes/app/app_defaults.py create mode 100644 mealie/schema/meal_plan/new_meal.py create mode 100644 mealie/services/_base_http_service/crud_http_mixins.py rename mealie/services/group_services/{group_mixins.py => group_utils.py} (91%) create mode 100644 mealie/services/group_services/meal_service.py create mode 100644 tests/integration_tests/user_group_tests/test_group_mealplan.py create mode 100644 tests/unit_tests/validator_tests/test_create_plan_entry.py diff --git a/frontend/api/class-interfaces/_base.ts b/frontend/api/class-interfaces/_base.ts index 3e945f67ec97..e7383434e328 100644 --- a/frontend/api/class-interfaces/_base.ts +++ b/frontend/api/class-interfaces/_base.ts @@ -12,15 +12,14 @@ export interface CrudAPIInterface { export interface CrudAPIMethodsInterface { // CRUD Methods - getAll(): any - createOne(): any - getOne(): any - updateOne(): any - patchOne(): any - deleteOne(): any + getAll(): any; + createOne(): any; + getOne(): any; + updateOne(): any; + patchOne(): any; + deleteOne(): any; } - export abstract class BaseAPI { requests: ApiRequestInstance; @@ -33,9 +32,9 @@ export abstract class BaseCRUDAPI extends BaseAPI implements CrudAPIInterf abstract baseRoute: string; abstract itemRoute(itemId: string | number): string; - async getAll(start = 0, limit = 9999) { + async getAll(start = 0, limit = 9999, params = {}) { return await this.requests.get(this.baseRoute, { - params: { start, limit }, + params: { start, limit, ...params }, }); } diff --git a/frontend/api/class-interfaces/cookbooks.ts b/frontend/api/class-interfaces/group-cookbooks.ts similarity index 100% rename from frontend/api/class-interfaces/cookbooks.ts rename to frontend/api/class-interfaces/group-cookbooks.ts diff --git a/frontend/api/class-interfaces/group-mealplan.ts b/frontend/api/class-interfaces/group-mealplan.ts new file mode 100644 index 000000000000..f81279427008 --- /dev/null +++ b/frontend/api/class-interfaces/group-mealplan.ts @@ -0,0 +1,32 @@ +import { BaseCRUDAPI } from "./_base"; + +const prefix = "/api"; + +const routes = { + mealplan: `${prefix}/groups/mealplans`, + mealplanId: (id: string | number) => `${prefix}/groups/mealplans/${id}`, +}; + +type PlanEntryType = "breakfast" | "lunch" | "dinner" | "snack"; + +export interface CreateMealPlan { + date: string; + entryType: PlanEntryType; + title: string; + text: string; + recipeId?: number; +} + +export interface UpdateMealPlan extends CreateMealPlan { + id: number; + groupId: number; +} + +export interface MealPlan extends UpdateMealPlan { + recipe: any; +} + +export class MealPlanAPI extends BaseCRUDAPI { + baseRoute = routes.mealplan; + itemRoute = routes.mealplanId; +} diff --git a/frontend/api/index.ts b/frontend/api/index.ts index 90605ca05f11..0671b11d1a67 100644 --- a/frontend/api/index.ts +++ b/frontend/api/index.ts @@ -10,10 +10,11 @@ import { UtilsAPI } from "./class-interfaces/utils"; import { NotificationsAPI } from "./class-interfaces/event-notifications"; import { FoodAPI } from "./class-interfaces/recipe-foods"; import { UnitAPI } from "./class-interfaces/recipe-units"; -import { CookbookAPI } from "./class-interfaces/cookbooks"; +import { CookbookAPI } from "./class-interfaces/group-cookbooks"; import { WebhooksAPI } from "./class-interfaces/group-webhooks"; import { AdminAboutAPI } from "./class-interfaces/admin-about"; import { RegisterAPI } from "./class-interfaces/user-registration"; +import { MealPlanAPI } from "./class-interfaces/group-mealplan"; import { ApiRequestInstance } from "~/types/api"; class AdminAPI { @@ -48,6 +49,7 @@ class Api { public cookbooks: CookbookAPI; public groupWebhooks: WebhooksAPI; public register: RegisterAPI; + public mealplans: MealPlanAPI; // Utils public upload: UploadFile; @@ -70,6 +72,7 @@ class Api { this.cookbooks = new CookbookAPI(requests); this.groupWebhooks = new WebhooksAPI(requests); this.register = new RegisterAPI(requests); + this.mealplans = new MealPlanAPI(requests); // Admin this.events = new EventsAPI(requests); diff --git a/frontend/components/Domain/Recipe/RecipeCard.vue b/frontend/components/Domain/Recipe/RecipeCard.vue index 9f76f2638b63..e1c0ebd0c8a5 100644 --- a/frontend/components/Domain/Recipe/RecipeCard.vue +++ b/frontend/components/Domain/Recipe/RecipeCard.vue @@ -30,6 +30,7 @@ + @@ -58,11 +59,13 @@ export default { }, rating: { type: Number, + required: false, default: 0, }, image: { type: String, - default: null, + required: false, + default: "abc123", }, route: { type: Boolean, diff --git a/frontend/components/Domain/Recipe/RecipeCardMobile.vue b/frontend/components/Domain/Recipe/RecipeCardMobile.vue index 23d9369258b0..fa99f6984434 100644 --- a/frontend/components/Domain/Recipe/RecipeCardMobile.vue +++ b/frontend/components/Domain/Recipe/RecipeCardMobile.vue @@ -9,36 +9,41 @@ @click="$emit('selected')" > - - - - {{ $globals.icons.primary }} - - + + + + + {{ $globals.icons.primary }} + + + {{ name }} {{ description }}
- - - - + + + + + +
+ diff --git a/frontend/components/Domain/Recipe/RecipeCategoryTagDialog.vue b/frontend/components/Domain/Recipe/RecipeCategoryTagDialog.vue index fd7dbaaaece2..0dd9e3ffa01b 100644 --- a/frontend/components/Domain/Recipe/RecipeCategoryTagDialog.vue +++ b/frontend/components/Domain/Recipe/RecipeCategoryTagDialog.vue @@ -35,7 +35,7 @@ - - \ No newline at end of file + + function forwardOneWeek() { + if (!state.today) return; + // @ts-ignore + state.today = addDays(state.today, +5); + } + + function backOneWeek() { + if (!state.today) return; + // @ts-ignore + state.today = addDays(state.today, -5); + } + + function onMoveCallback(evt: SortableEvent) { + // Adapted From https://github.com/SortableJS/Vue.Draggable/issues/1029 + const ogEvent: DragEvent = (evt as any).originalEvent; + + if (ogEvent && ogEvent.type !== "drop") { + // The drop was cancelled, unsure if anything needs to be done? + console.log("Cancel Move Event"); + } else { + // A Meal was moved, set the new date value and make a update request and refresh the meals + const fromMealsByIndex = evt.from.getAttribute("data-index"); + const toMealsByIndex = evt.to.getAttribute("data-index"); + + if (fromMealsByIndex) { + // @ts-ignore + const mealData = mealsByDate.value[fromMealsByIndex].meals[evt.oldIndex as number]; + // @ts-ignore + const destDate = mealsByDate.value[toMealsByIndex].date; + + mealData.date = format(destDate, "yyyy-MM-dd"); + + actions.updateOne(mealData); + } + } + } + + const mealsByDate = computed(() => { + return days.value.map((day) => { + return { date: day, meals: filterMealByDate(day as any) }; + }); + }); + + const weekRange = computed(() => { + // @ts-ignore - Not Sure Why This is not working + const end = addDays(state.today, 2); + // @ts-ignore - Not sure why the type is invalid + const start = subDays(state.today, 2); + return { start, end, today: state.today }; + }); + + const days = computed(() => { + if (weekRange.value?.start === null) return []; + return Array.from(Array(8).keys()).map( + // @ts-ignore + (i) => new Date(weekRange.value.start.getTime() + i * 24 * 60 * 60 * 1000) + ); + }); + + const newMeal = reactive({ + date: null, + title: "", + text: "", + recipeId: null, + }); + + return { + mealplans, + actions, + newMeal, + allRecipes, + ...toRefs(state), + mealsByDate, + onMoveCallback, + backOneWeek, + forwardOneWeek, + weekRange, + days, + }; + }, +}); + + \ No newline at end of file diff --git a/frontend/pages/meal-plan/this-week.vue b/frontend/pages/meal-plan/this-week.vue index bb6aebe3f129..411c8fc22c68 100644 --- a/frontend/pages/meal-plan/this-week.vue +++ b/frontend/pages/meal-plan/this-week.vue @@ -1,16 +1,16 @@ +
+ +import { defineComponent } from "@nuxtjs/composition-api"; + +export default defineComponent({ + setup() { + return {}; + }, +}); + \ No newline at end of file + \ No newline at end of file diff --git a/frontend/pages/search.vue b/frontend/pages/search.vue index 15cfa49e79d7..af6e74ef73c9 100644 --- a/frontend/pages/search.vue +++ b/frontend/pages/search.vue @@ -67,7 +67,7 @@