Feature/automated meal planner (#939)

* cleanup oversized buttons

* add get all by category function to reciep repos

* fix shopping-list can_merge logic

* use randomized data for testing

* add random getter to repository for meal-planner

* add stub route for random meals

* cleanup global namespace

* add rules database type

* fix type

* add plan rules schema

* test plan rules methods

* add mealplan rules controller

* add new repository

* update frontend types

* formatting

* fix regression

* update autogenerated types

* add api class for mealplan rules

* add tests and fix bugs

* fix data returns

* proof of concept rules editor

* add tag support

* remove old group categories

* add tag support

* implement random by rules api

* change snack to sides

* remove incorrect typing

* split repo for custom methods

* fix query and use and_ clause

* use repo function

* remove old test

* update changelog
This commit is contained in:
Hayden
2022-02-07 19:03:11 -09:00
committed by GitHub
parent 40d1f586cd
commit d1024e272d
43 changed files with 1153 additions and 175 deletions

View File

@@ -12,6 +12,7 @@ router = APIRouter(prefix="/categories", tags=["Categories: CRUD"])
class CategorySummary(BaseModel):
id: int
slug: str
name: str

View File

@@ -8,7 +8,8 @@ from . import (
controller_invitations,
controller_labels,
controller_mealplan,
controller_meaplan_config,
controller_mealplan_config,
controller_mealplan_rules,
controller_migrations,
controller_shopping_lists,
controller_webhooks,
@@ -17,9 +18,10 @@ from . import (
router = APIRouter()
router.include_router(controller_group_self_service.router)
router.include_router(controller_mealplan_rules.router)
router.include_router(controller_mealplan_config.router)
router.include_router(controller_mealplan.router)
router.include_router(controller_cookbooks.router)
router.include_router(controller_meaplan_config.router)
router.include_router(controller_webhooks.router)
router.include_router(controller_invitations.router)
router.include_router(controller_migrations.router)

View File

@@ -2,7 +2,7 @@ from datetime import date, timedelta
from functools import cached_property
from typing import Type
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from mealie.core.exceptions import mealie_registered_exceptions
from mealie.repos.repository_meals import RepositoryMeals
@@ -10,6 +10,10 @@ from mealie.routes._base import BaseUserController, controller
from mealie.routes._base.mixins import CrudMixins
from mealie.schema import mapper
from mealie.schema.meal_plan import CreatePlanEntry, ReadPlanEntry, SavePlanEntry, UpdatePlanEntry
from mealie.schema.meal_plan.new_meal import CreatRandomEntry
from mealie.schema.meal_plan.plan_rules import PlanRulesDay
from mealie.schema.recipe.recipe import Recipe
from mealie.schema.response.responses import ErrorResponse
router = APIRouter(prefix="/groups/mealplans", tags=["Groups: Mealplans"])
@@ -34,10 +38,54 @@ class GroupMealplanController(BaseUserController):
self.registered_exceptions,
)
@router.get("/today", tags=["Groups: Mealplans"])
@router.get("/today")
def get_todays_meals(self):
return self.repo.get_today(group_id=self.group_id)
@router.post("/random", response_model=ReadPlanEntry)
def create_random_meal(self, data: CreatRandomEntry):
"""
create_random_meal is a route that provides the randomized funcitonality for mealplaners.
It operates by following the rules setout in the Groups mealplan settings. If not settings
are set, it will default return any random meal.
Refer to the mealplan settings routes for more information on how rules can be applied
to the random meal selector.
"""
# Get relavent group rules
rules = self.repos.group_meal_plan_rules.by_group(self.group_id).get_rules(
PlanRulesDay.from_date(data.date), data.entry_type.value
)
recipe_repo = self.repos.recipes.by_group(self.group_id)
random_recipes: Recipe = []
if not rules: # If no rules are set, return any random recipe from the group
random_recipes = recipe_repo.get_random()
else: # otherwise construct a query based on the rules
tags = []
categories = []
for rule in rules:
if rule.tags:
tags.extend(rule.tags)
if rule.categories:
categories.extend(rule.categories)
if tags or categories:
random_recipes = self.repos.recipes.by_group(self.group_id).get_random_by_categories_and_tags(
categories, tags
)
else:
random_recipes = recipe_repo.get_random()
try:
recipe = random_recipes[0]
return self.mixins.create_one(
SavePlanEntry(date=data.date, entry_type=data.entry_type, recipe_id=recipe.id, group_id=self.group_id)
)
except IndexError:
raise HTTPException(status_code=404, detail=ErrorResponse.respond(message="No recipes match your rules"))
@router.get("", response_model=list[ReadPlanEntry])
def get_all(self, start: date = None, limit: date = None):
start = start or date.today() - timedelta(days=999)

View File

@@ -0,0 +1,44 @@
from functools import cached_property
from pydantic import UUID4
from mealie.routes._base.abc_controller import BaseUserController
from mealie.routes._base.controller import controller
from mealie.routes._base.mixins import CrudMixins
from mealie.routes._base.routers import UserAPIRouter
from mealie.schema import mapper
from mealie.schema.meal_plan.plan_rules import PlanRulesCreate, PlanRulesOut, PlanRulesSave
router = UserAPIRouter(prefix="/groups/mealplans/rules", tags=["Groups: Mealplan Rules"])
@controller(router)
class GroupMealplanConfigController(BaseUserController):
@cached_property
def repo(self):
return self.repos.group_meal_plan_rules.by_group(self.group_id)
@cached_property
def mixins(self):
return CrudMixins[PlanRulesCreate, PlanRulesOut, PlanRulesOut](self.repo, self.deps.logger)
@router.get("", response_model=list[PlanRulesOut])
def get_all(self):
return self.repo.get_all(override_schema=PlanRulesOut)
@router.post("", response_model=PlanRulesOut, status_code=201)
def create_one(self, data: PlanRulesCreate):
save = mapper.cast(data, PlanRulesSave, group_id=self.group.id)
return self.mixins.create_one(save)
@router.get("/{item_id}", response_model=PlanRulesOut)
def get_one(self, item_id: UUID4):
return self.mixins.get_one(item_id)
@router.put("/{item_id}", response_model=PlanRulesOut)
def update_one(self, item_id: UUID4, data: PlanRulesCreate):
return self.mixins.update_one(data, item_id)
@router.delete("/{item_id}", response_model=PlanRulesOut)
def delete_one(self, item_id: UUID4):
return self.mixins.delete_one(item_id) # type: ignore