feat(backend): rewrite mealplanner with simple api (#683)

* feat(backend):  new meal-planner feature

* feat(frontend):  new meal plan feature

* refactor(backend): ♻️ refactor base services classes and add mixins for crud

* feat(frontend):  add UI/API for mealplanner

* feat(backend):  add get_today and get_slice options for mealplanner

* test(backend):  add and update group mealplanner tests

* fix(backend): 🐛 Fix recipe_id column type for PG

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden
2021-09-12 11:05:09 -08:00
committed by GitHub
parent bdaf758712
commit b542583303
46 changed files with 869 additions and 255 deletions

View File

@@ -2,6 +2,7 @@ from abc import ABC, abstractmethod
from typing import Any, Callable, Generic, Type, TypeVar
from fastapi import BackgroundTasks, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy.orm.session import Session
from mealie.core.config import get_app_dirs, get_settings
@@ -113,6 +114,25 @@ class BaseHttpService(Generic[T, D], ABC):
self._group_id_cache = group.id
return self._group_id_cache
def cast(self, item: BaseModel, dest, assign_owner=True) -> T:
"""cast a pydantic model to the destination type
Args:
item (BaseModel): A pydantic model containing data
dest ([type]): A type to cast the data to
assign_owner (bool, optional): If true, will assign the user_id and group_id to the dest type. Defaults to True.
Returns:
TypeVar(dest): Returns the destionation model type
"""
data = item.dict()
if assign_owner:
data["user_id"] = self.user.id
data["group_id"] = self.group_id
return dest(**data)
def assert_existing(self, id: T) -> None:
self.populate_item(id)
self._check_item()
@@ -135,30 +155,3 @@ class BaseHttpService(Generic[T, D], ABC):
raise NotImplementedError("`event_func` must be set by child class")
self.background_tasks.add_task(self.__class__.event_func, title, message, self.session)
# Generic CRUD Functions
def _create_one(self, data: Any, exception_msg="generic-create-error") -> D:
try:
self.item = self.db_access.create(self.session, data)
except Exception as ex:
logger.exception(ex)
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": exception_msg, "exception": str(ex)})
return self.item
def _update_one(self, data: Any, id: int = None) -> D:
if not self.item:
return
target_id = id or self.item.id
self.item = self.db_access.update(self.session, target_id, data)
return self.item
def _delete_one(self, id: int = None) -> D:
if not self.item:
return
target_id = id or self.item.id
self.item = self.db_access.delete(self.session, target_id)
return self.item

View File

@@ -0,0 +1,49 @@
from typing import Generic, TypeVar
from fastapi import HTTPException, status
from pydantic import BaseModel
from sqlalchemy.orm import Session
from mealie.core.root_logger import get_logger
from mealie.db.data_access_layer.db_access import DatabaseAccessLayer
C = TypeVar("C", bound=BaseModel)
R = TypeVar("R", bound=BaseModel)
U = TypeVar("U", bound=BaseModel)
DAL = TypeVar("DAL", bound=DatabaseAccessLayer)
logger = get_logger()
class CrudHttpMixins(Generic[C, R, U]):
item: C
session: Session
dal: DAL
def _create_one(self, data: C, exception_msg="generic-create-error") -> R:
try:
self.item = self.dal.create(self.session, data)
except Exception as ex:
logger.exception(ex)
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": exception_msg, "exception": str(ex)})
return self.item
def _update_one(self, data: U, item_id: int = None) -> R:
if not self.item:
return
target_id = item_id or self.item.id
self.item = self.dal.update(self.session, target_id, data)
return self.item
def _patch_one(self) -> None:
raise NotImplementedError
def _delete_one(self, item_id: int = None) -> None:
if not self.item:
return
target_id = item_id or self.item.id
self.item = self.dal.delete(self.session, target_id)
return self.item

View File

@@ -75,6 +75,7 @@ class RouterFactory(APIRouter):
methods=["POST"],
response_model=self.schema,
summary="Create One",
status_code=201,
description=inspect.cleandoc(self.service.create_one.__doc__ or ""),
)
@@ -162,7 +163,9 @@ class RouterFactory(APIRouter):
self.routes.remove(route)
def _get_all(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
def route(service: S = Depends(self.service.private)) -> T: # type: ignore
service_dep = getattr(self.service, "get_all_dep", self.service.private)
def route(service: S = Depends(service_dep)) -> T: # type: ignore
return service.get_all()
return route

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from mealie.core.root_logger import get_logger
from mealie.db.database import get_database
from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
from mealie.services._base_http_service.http_services import UserHttpService
from mealie.services.events import create_group_event
from mealie.utils.error_messages import ErrorMessages
@@ -10,13 +11,18 @@ from mealie.utils.error_messages import ErrorMessages
logger = get_logger(module=__name__)
class CookbookService(UserHttpService[int, ReadCookBook]):
class CookbookService(
UserHttpService[int, ReadCookBook],
CrudHttpMixins[CreateCookBook, ReadCookBook, UpdateCookBook],
):
event_func = create_group_event
_restrict_by_group = True
_schema = ReadCookBook
db_access = get_database().cookbooks
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dal = get_database().cookbooks
def populate_item(self, item_id: int) -> RecipeCookBook:
try:
@@ -36,7 +42,7 @@ class CookbookService(UserHttpService[int, ReadCookBook]):
return items
def create_one(self, data: CreateCookBook) -> ReadCookBook:
data = SaveCookBook(group_id=self.group_id, **data.dict())
data = self.cast(data, SaveCookBook)
return self._create_one(data, ErrorMessages.cookbook_create_failure)
def update_one(self, data: UpdateCookBook, id: int = None) -> ReadCookBook:

View File

@@ -46,7 +46,6 @@ class GroupSelfService(UserHttpService[int, str]):
def update_categories(self, new_categories: list[CategoryBase]):
self.item.categories = new_categories
return self.db.groups.update(self.session, self.group_id, self.item)
def update_preferences(self, new_preferences: UpdateGroupPreferences):

View File

@@ -7,7 +7,7 @@ def create_new_group(session, g_base: GroupBase, g_preferences: CreateGroupPrefe
db = get_database()
created_group = db.groups.create(session, g_base)
g_preferences = g_preferences or CreateGroupPreferences(group_id=0)
g_preferences = g_preferences or CreateGroupPreferences(group_id=0) # Assign Temporary ID before group is created
g_preferences.group_id = created_group.id

View File

@@ -0,0 +1,47 @@
from __future__ import annotations
from datetime import date
from mealie.core.root_logger import get_logger
from mealie.db.database import get_database
from mealie.schema.meal_plan import CreatePlanEntry, ReadPlanEntry, SavePlanEntry, UpdatePlanEntry
from .._base_http_service.crud_http_mixins import CrudHttpMixins
from .._base_http_service.http_services import UserHttpService
from ..events import create_group_event
logger = get_logger(module=__name__)
class MealService(UserHttpService[int, ReadPlanEntry], CrudHttpMixins[CreatePlanEntry, ReadPlanEntry, UpdatePlanEntry]):
event_func = create_group_event
_restrict_by_group = True
_schema = ReadPlanEntry
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dal = get_database().meals
def populate_item(self, id: int) -> ReadPlanEntry:
self.item = self.db.meals.get_one(self.session, id)
return self.item
def get_slice(self, start: date = None, end: date = None) -> list[ReadPlanEntry]:
# 2 days ago
return self.db.meals.get_slice(self.session, start, end, group_id=self.group_id)
def get_today(self) -> list[ReadPlanEntry]:
return self.db.meals.get_today(self.session, group_id=self.group_id)
def create_one(self, data: CreatePlanEntry) -> ReadPlanEntry:
data = self.cast(data, SavePlanEntry)
return self._create_one(data)
def update_one(self, data: UpdatePlanEntry, id: int = None) -> ReadPlanEntry:
target_id = id or self.item.id
return self._update_one(data, target_id)
def delete_one(self, id: int = None) -> ReadPlanEntry:
target_id = id or self.item.id
return self._delete_one(target_id)

View File

@@ -1,23 +1,25 @@
from __future__ import annotations
from fastapi import HTTPException, status
from mealie.core.root_logger import get_logger
from mealie.db.database import get_database
from mealie.schema.group import ReadWebhook
from mealie.schema.group.webhook import CreateWebhook, SaveWebhook
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
from mealie.services._base_http_service.http_services import UserHttpService
from mealie.services.events import create_group_event
logger = get_logger(module=__name__)
class WebhookService(UserHttpService[int, ReadWebhook]):
class WebhookService(UserHttpService[int, ReadWebhook], CrudHttpMixins[ReadWebhook, CreateWebhook, CreateWebhook]):
event_func = create_group_event
_restrict_by_group = True
_schema = ReadWebhook
_create_schema = CreateWebhook
_update_schema = CreateWebhook
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dal = get_database().webhooks
def populate_item(self, id: int) -> ReadWebhook:
self.item = self.db.webhooks.get_one(self.session, id)
@@ -27,29 +29,11 @@ class WebhookService(UserHttpService[int, ReadWebhook]):
return self.db.webhooks.get(self.session, self.group_id, match_key="group_id", limit=9999)
def create_one(self, data: CreateWebhook) -> ReadWebhook:
try:
self.item = self.db.webhooks.create(self.session, SaveWebhook(group_id=self.group_id, **data.dict()))
except Exception as ex:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, detail={"message": "WEBHOOK_CREATION_ERROR", "exception": str(ex)}
)
data = self.cast(data, SaveWebhook)
return self._create_one(data)
return self.item
def update_one(self, data: CreateWebhook, id: int = None) -> ReadWebhook:
if not self.item:
return
target_id = id or self.item.id
self.item = self.db.webhooks.update(self.session, target_id, data)
return self.item
def update_one(self, data: CreateWebhook, item_id: int = None) -> ReadWebhook:
return self._update_one(data, item_id)
def delete_one(self, id: int = None) -> ReadWebhook:
if not self.item:
return
target_id = id or self.item.id
self.db.webhooks.delete(self.session, target_id)
return self.item
return self._delete_one(id)

View File

@@ -7,7 +7,7 @@ from mealie.schema.user.registration import CreateUserRegistration
from mealie.schema.user.user import GroupBase, GroupInDB, PrivateUser, UserIn
from mealie.services._base_http_service.http_services import PublicHttpService
from mealie.services.events import create_user_event
from mealie.services.group_services.group_mixins import create_new_group
from mealie.services.group_services.group_utils import create_new_group
logger = get_logger(module=__name__)