mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-08 00:43:12 -05:00
Refactor/group page (#666)
* refactor(backend): ♻️ Refactor base class to be abstract and create a router factory method * feat(frontend): ✨ add group edit * refactor(backend): ✨ add group edit support Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
from .base_http_service import *
|
||||
from .base_service import *
|
||||
from .router_factory import *
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Callable, Generic, TypeVar
|
||||
|
||||
from fastapi import BackgroundTasks, Depends
|
||||
from fastapi import BackgroundTasks, Depends, HTTPException, status
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.config import get_app_dirs, get_settings
|
||||
@@ -16,13 +17,13 @@ T = TypeVar("T")
|
||||
D = TypeVar("D")
|
||||
|
||||
|
||||
class BaseHttpService(Generic[T, D]):
|
||||
class BaseHttpService(Generic[T, D], ABC):
|
||||
"""The BaseHttpService class is a generic class that can be used to create
|
||||
http services that are injected via `Depends` into a route function. To use,
|
||||
you must define the Generic type arguments:
|
||||
|
||||
`T`: The type passed into the *_existing functions (e.g. id) which is then passed into assert_existing
|
||||
`D`: Not yet implemented
|
||||
`D`: Item returned from database layer
|
||||
|
||||
Child Requirements:
|
||||
Define the following functions:
|
||||
@@ -32,8 +33,29 @@ class BaseHttpService(Generic[T, D]):
|
||||
`event_func`: A function that is called when an event is created.
|
||||
"""
|
||||
|
||||
item: D = None
|
||||
|
||||
# Function that Generate Corrsesponding Routes through RouterFactor
|
||||
get_all: Callable = None
|
||||
create_one: Callable = None
|
||||
update_one: Callable = None
|
||||
update_many: Callable = None
|
||||
populate_item: Callable = None
|
||||
delete_one: Callable = None
|
||||
delete_all: Callable = None
|
||||
|
||||
# Type Definitions
|
||||
_schema = None
|
||||
_create_schema = None
|
||||
_update_schema = None
|
||||
|
||||
# Function called to create a server side event
|
||||
event_func: Callable = None
|
||||
|
||||
# Config
|
||||
_restrict_by_group = False
|
||||
_group_id_cache = None
|
||||
|
||||
def __init__(self, session: Session, user: PrivateUser, background_tasks: BackgroundTasks = None) -> None:
|
||||
self.session = session or SessionLocal()
|
||||
self.user = user
|
||||
@@ -45,33 +67,32 @@ class BaseHttpService(Generic[T, D]):
|
||||
self.app_dirs = get_app_dirs()
|
||||
self.settings = get_settings()
|
||||
|
||||
def assert_existing(self, data: T) -> None:
|
||||
raise NotImplementedError("`assert_existing` must by implemented by child class")
|
||||
|
||||
def _create_event(self, title: str, message: str) -> None:
|
||||
if not self.__class__.event_func:
|
||||
raise NotImplementedError("`event_func` must be set by child class")
|
||||
|
||||
self.background_tasks.add_task(self.__class__.event_func, title, message, self.session)
|
||||
@property
|
||||
def group_id(self):
|
||||
# TODO: Populate Group in Private User Call WARNING: May require significant refactoring
|
||||
if not self._group_id_cache:
|
||||
group = self.db.groups.get(self.session, self.user.group, "name")
|
||||
self._group_id_cache = group.id
|
||||
return self._group_id_cache
|
||||
|
||||
@classmethod
|
||||
def read_existing(cls, id: T, deps: ReadDeps = Depends()):
|
||||
def read_existing(cls, item_id: T, deps: ReadDeps = Depends()):
|
||||
"""
|
||||
Used for dependency injection for routes that require an existing recipe. If the recipe doesn't exist
|
||||
or the user doens't not have the required permissions, the proper HTTP Status code will be raised.
|
||||
"""
|
||||
new_class = cls(deps.session, deps.user, deps.bg_task)
|
||||
new_class.assert_existing(id)
|
||||
new_class.assert_existing(item_id)
|
||||
return new_class
|
||||
|
||||
@classmethod
|
||||
def write_existing(cls, id: T, deps: WriteDeps = Depends()):
|
||||
def write_existing(cls, item_id: T, deps: WriteDeps = Depends()):
|
||||
"""
|
||||
Used for dependency injection for routes that require an existing recipe. The only difference between
|
||||
read_existing and write_existing is that the user is required to be logged in on write_existing method.
|
||||
"""
|
||||
new_class = cls(deps.session, deps.user, deps.bg_task)
|
||||
new_class.assert_existing(id)
|
||||
new_class.assert_existing(item_id)
|
||||
return new_class
|
||||
|
||||
@classmethod
|
||||
@@ -87,3 +108,27 @@ class BaseHttpService(Generic[T, D]):
|
||||
A Base instance to be used as a router dependency
|
||||
"""
|
||||
return cls(deps.session, deps.user, deps.bg_task)
|
||||
|
||||
@abstractmethod
|
||||
def populate_item(self) -> None:
|
||||
...
|
||||
|
||||
def assert_existing(self, id: T) -> None:
|
||||
self.populate_item(id)
|
||||
self._check_item()
|
||||
|
||||
def _check_item(self) -> None:
|
||||
if not self.item:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if self.__class__._restrict_by_group:
|
||||
group_id = getattr(self.item, "group_id", False)
|
||||
|
||||
if not group_id or group_id != self.group_id:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def _create_event(self, title: str, message: str) -> None:
|
||||
if not self.__class__.event_func:
|
||||
raise NotImplementedError("`event_func` must be set by child class")
|
||||
|
||||
self.background_tasks.add_task(self.__class__.event_func, title, message, self.session)
|
||||
|
||||
190
mealie/services/base_http_service/router_factory.py
Normal file
190
mealie/services/base_http_service/router_factory.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from typing import Any, Callable, Optional, Sequence, Type, TypeVar
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.params import Depends
|
||||
from fastapi.types import DecoratedCallable
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .base_http_service import BaseHttpService
|
||||
|
||||
""""
|
||||
This code is largely based off of the FastAPI Crud Router
|
||||
https://github.com/awtkns/fastapi-crudrouter/blob/master/fastapi_crudrouter/core/_base.py
|
||||
"""
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
S = TypeVar("S", bound=BaseHttpService)
|
||||
DEPENDENCIES = Optional[Sequence[Depends]]
|
||||
|
||||
|
||||
class RouterFactory(APIRouter):
|
||||
schema: Type[T]
|
||||
create_schema: Type[T]
|
||||
update_schema: Type[T]
|
||||
_base_path: str = "/"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
service: Type[S],
|
||||
prefix: Optional[str] = None,
|
||||
tags: Optional[list[str]] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
self.service: Type[S] = service
|
||||
self.schema: Type[T] = service._schema
|
||||
|
||||
# HACK: Special Case for Coobooks, not sure this is a good way to handle the abstraction :/
|
||||
if hasattr(self.service, "_get_one_schema"):
|
||||
self.get_one_schema = self.service._get_one_schema
|
||||
else:
|
||||
self.get_one_schema = self.schema
|
||||
|
||||
self.update_schema: Type[T] = service._update_schema
|
||||
self.create_schema: Type[T] = service._create_schema
|
||||
|
||||
prefix = str(prefix or self.schema.__name__).lower()
|
||||
prefix = self._base_path + prefix.strip("/")
|
||||
tags = tags or [prefix.strip("/").capitalize()]
|
||||
|
||||
super().__init__(prefix=prefix, tags=tags, **kwargs)
|
||||
|
||||
if self.service.get_all:
|
||||
self._add_api_route(
|
||||
"",
|
||||
self._get_all(),
|
||||
methods=["GET"],
|
||||
response_model=Optional[list[self.schema]], # type: ignore
|
||||
summary="Get All",
|
||||
)
|
||||
|
||||
if self.service.create_one:
|
||||
self._add_api_route(
|
||||
"",
|
||||
self._create(),
|
||||
methods=["POST"],
|
||||
response_model=self.schema,
|
||||
summary="Create One",
|
||||
)
|
||||
|
||||
if self.service.update_many:
|
||||
self._add_api_route(
|
||||
"",
|
||||
self._update_many(),
|
||||
methods=["PUT"],
|
||||
response_model=Optional[list[self.schema]], # type: ignore
|
||||
summary="Update Many",
|
||||
)
|
||||
|
||||
if self.service.delete_all:
|
||||
self._add_api_route(
|
||||
"",
|
||||
self._delete_all(),
|
||||
methods=["DELETE"],
|
||||
response_model=Optional[list[self.schema]], # type: ignore
|
||||
summary="Delete All",
|
||||
)
|
||||
|
||||
if self.service.populate_item:
|
||||
self._add_api_route(
|
||||
"/{item_id}",
|
||||
self._get_one(),
|
||||
methods=["GET"],
|
||||
response_model=self.get_one_schema,
|
||||
summary="Get One",
|
||||
)
|
||||
|
||||
if self.service.update_one:
|
||||
self._add_api_route(
|
||||
"/{item_id}",
|
||||
self._update(),
|
||||
methods=["PUT"],
|
||||
response_model=self.schema,
|
||||
summary="Update One",
|
||||
)
|
||||
|
||||
if self.service.delete_one:
|
||||
self._add_api_route(
|
||||
"/{item_id}",
|
||||
self._delete_one(),
|
||||
methods=["DELETE"],
|
||||
response_model=self.schema,
|
||||
summary="Delete One",
|
||||
)
|
||||
|
||||
def _add_api_route(self, path: str, endpoint: Callable[..., Any], **kwargs: Any) -> None:
|
||||
dependencies = []
|
||||
super().add_api_route(path, endpoint, dependencies=dependencies, **kwargs)
|
||||
|
||||
def api_route(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
"""Overrides and exiting route if it exists"""
|
||||
methods = kwargs["methods"] if "methods" in kwargs else ["GET"]
|
||||
self.remove_api_route(path, methods)
|
||||
return super().api_route(path, *args, **kwargs)
|
||||
|
||||
def get(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
self.remove_api_route(path, ["Get"])
|
||||
return super().get(path, *args, **kwargs)
|
||||
|
||||
def post(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
self.remove_api_route(path, ["POST"])
|
||||
return super().post(path, *args, **kwargs)
|
||||
|
||||
def put(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
self.remove_api_route(path, ["PUT"])
|
||||
return super().put(path, *args, **kwargs)
|
||||
|
||||
def delete(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
self.remove_api_route(path, ["DELETE"])
|
||||
return super().delete(path, *args, **kwargs)
|
||||
|
||||
def remove_api_route(self, path: str, methods: list[str]) -> None:
|
||||
methods_ = set(methods)
|
||||
|
||||
for route in self.routes:
|
||||
if route.path == f"{self.prefix}{path}" and route.methods == methods_:
|
||||
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
|
||||
return service.get_all()
|
||||
|
||||
return route
|
||||
|
||||
def _get_one(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
def route(service: S = Depends(self.service.write_existing)) -> T: # type: ignore
|
||||
return service.item
|
||||
|
||||
return route
|
||||
|
||||
def _create(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
def route(data: self.create_schema, service: S = Depends(self.service.private)) -> T: # type: ignore
|
||||
return service.create_one(data)
|
||||
|
||||
return route
|
||||
|
||||
def _update(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
def route(data: self.update_schema, service: S = Depends(self.service.write_existing)) -> T: # type: ignore
|
||||
return service.update_one(data)
|
||||
|
||||
return route
|
||||
|
||||
def _update_many(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
def route(data: list[self.update_schema], service: S = Depends(self.service.write_existing)) -> T: # type: ignore
|
||||
return service.update_many(data)
|
||||
|
||||
return route
|
||||
|
||||
def _delete_one(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
def route(service: S = Depends(self.service.write_existing)) -> T: # type: ignore
|
||||
return service.delete_one()
|
||||
|
||||
return route
|
||||
|
||||
def _delete_all(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def get_routes() -> list[str]:
|
||||
return ["get_all", "create", "delete_all", "get_one", "update", "delete_one"]
|
||||
@@ -3,55 +3,33 @@ from __future__ import annotations
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook
|
||||
from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
|
||||
from mealie.services.base_http_service.base_http_service import BaseHttpService
|
||||
from mealie.services.events import create_group_event
|
||||
|
||||
logger = get_logger(module=__name__)
|
||||
|
||||
|
||||
class CookbookService(BaseHttpService[int, str]):
|
||||
"""
|
||||
Class Methods:
|
||||
`read_existing`: Reads an existing recipe from the database.
|
||||
`write_existing`: Updates an existing recipe in the database.
|
||||
`base`: Requires write permissions, but doesn't perform recipe checks
|
||||
"""
|
||||
|
||||
class CookbookService(BaseHttpService[int, ReadCookBook]):
|
||||
event_func = create_group_event
|
||||
cookbook: ReadCookBook # Required for proper type hints
|
||||
_restrict_by_group = True
|
||||
|
||||
_group_id_cache = None
|
||||
_schema = ReadCookBook
|
||||
_create_schema = CreateCookBook
|
||||
_update_schema = UpdateCookBook
|
||||
_get_one_schema = RecipeCookBook
|
||||
|
||||
@property
|
||||
def group_id(self):
|
||||
# TODO: Populate Group in Private User Call WARNING: May require significant refactoring
|
||||
if not self._group_id_cache:
|
||||
group = self.db.groups.get(self.session, self.user.group, "name")
|
||||
print(group)
|
||||
self._group_id_cache = group.id
|
||||
return self._group_id_cache
|
||||
|
||||
def assert_existing(self, id: str):
|
||||
self.populate_cookbook(id)
|
||||
|
||||
if not self.cookbook:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if self.cookbook.group_id != self.group_id:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def populate_cookbook(self, id: int | str):
|
||||
def populate_item(self, id: int | str):
|
||||
try:
|
||||
id = int(id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if isinstance(id, int):
|
||||
self.cookbook = self.db.cookbooks.get_one(self.session, id, override_schema=RecipeCookBook)
|
||||
self.item = self.db.cookbooks.get_one(self.session, id, override_schema=RecipeCookBook)
|
||||
|
||||
else:
|
||||
self.cookbook = self.db.cookbooks.get_one(self.session, id, key="slug", override_schema=RecipeCookBook)
|
||||
self.item = self.db.cookbooks.get_one(self.session, id, key="slug", override_schema=RecipeCookBook)
|
||||
|
||||
def get_all(self) -> list[ReadCookBook]:
|
||||
items = self.db.cookbooks.get(self.session, self.group_id, "group_id", limit=999)
|
||||
@@ -60,22 +38,22 @@ class CookbookService(BaseHttpService[int, str]):
|
||||
|
||||
def create_one(self, data: CreateCookBook) -> ReadCookBook:
|
||||
try:
|
||||
self.cookbook = self.db.cookbooks.create(self.session, SaveCookBook(group_id=self.group_id, **data.dict()))
|
||||
self.item = self.db.cookbooks.create(self.session, SaveCookBook(group_id=self.group_id, **data.dict()))
|
||||
except Exception as ex:
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST, detail={"message": "PAGE_CREATION_ERROR", "exception": str(ex)}
|
||||
)
|
||||
|
||||
return self.cookbook
|
||||
return self.item
|
||||
|
||||
def update_one(self, data: CreateCookBook, id: int = None) -> ReadCookBook:
|
||||
if not self.cookbook:
|
||||
if not self.item:
|
||||
return
|
||||
|
||||
target_id = id or self.cookbook.id
|
||||
self.cookbook = self.db.cookbooks.update(self.session, target_id, data)
|
||||
target_id = id or self.item.id
|
||||
self.item = self.db.cookbooks.update(self.session, target_id, data)
|
||||
|
||||
return self.cookbook
|
||||
return self.item
|
||||
|
||||
def update_many(self, data: list[ReadCookBook]) -> list[ReadCookBook]:
|
||||
updated = []
|
||||
@@ -87,10 +65,10 @@ class CookbookService(BaseHttpService[int, str]):
|
||||
return updated
|
||||
|
||||
def delete_one(self, id: int = None) -> ReadCookBook:
|
||||
if not self.cookbook:
|
||||
if not self.item:
|
||||
return
|
||||
|
||||
target_id = id or self.cookbook.id
|
||||
self.cookbook = self.db.cookbooks.delete(self.session, target_id)
|
||||
target_id = id or self.item.id
|
||||
self.item = self.db.cookbooks.delete(self.session, target_id)
|
||||
|
||||
return self.cookbook
|
||||
return self.item
|
||||
|
||||
2
mealie/services/group/__init__.py
Normal file
2
mealie/services/group/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .group_service import *
|
||||
from .webhook_service import *
|
||||
47
mealie/services/group/group_service.py
Normal file
47
mealie/services/group/group_service.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
|
||||
from mealie.core.dependencies.grouped import WriteDeps
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.schema.recipe.recipe_category import CategoryBase
|
||||
from mealie.schema.user.user import GroupInDB
|
||||
from mealie.services.base_http_service.base_http_service import BaseHttpService
|
||||
from mealie.services.events import create_group_event
|
||||
|
||||
logger = get_logger(module=__name__)
|
||||
|
||||
|
||||
class GroupSelfService(BaseHttpService[int, str]):
|
||||
_restrict_by_group = True
|
||||
event_func = create_group_event
|
||||
item: GroupInDB
|
||||
|
||||
@classmethod
|
||||
def read_existing(cls, deps: WriteDeps = Depends()):
|
||||
"""Override parent method to remove `item_id` from arguments"""
|
||||
return super().read_existing(item_id=0, deps=deps)
|
||||
|
||||
@classmethod
|
||||
def write_existing(cls, deps: WriteDeps = Depends()):
|
||||
"""Override parent method to remove `item_id` from arguments"""
|
||||
return super().write_existing(item_id=0, deps=deps)
|
||||
|
||||
def assert_existing(self, _: str = None):
|
||||
self.populate_item()
|
||||
|
||||
if not self.item:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if self.item.id != self.group_id:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def populate_item(self, _: str = None):
|
||||
self.item = self.db.groups.get(self.session, self.group_id)
|
||||
|
||||
def update_categories(self, new_categories: list[CategoryBase]):
|
||||
if not self.item:
|
||||
return
|
||||
self.item.categories = new_categories
|
||||
|
||||
return self.db.groups.update(self.session, self.group_id, self.item)
|
||||
54
mealie/services/group/webhook_service.py
Normal file
54
mealie/services/group/webhook_service.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.schema.group import ReadWebhook
|
||||
from mealie.schema.group.webhook import CreateWebhook, SaveWebhook
|
||||
from mealie.services.base_http_service.base_http_service import BaseHttpService
|
||||
from mealie.services.events import create_group_event
|
||||
|
||||
logger = get_logger(module=__name__)
|
||||
|
||||
|
||||
class WebhookService(BaseHttpService[int, ReadWebhook]):
|
||||
event_func = create_group_event
|
||||
_restrict_by_group = True
|
||||
|
||||
_schema = ReadWebhook
|
||||
_create_schema = CreateWebhook
|
||||
_update_schema = CreateWebhook
|
||||
|
||||
def populate_item(self, id: int | str):
|
||||
self.item = self.db.webhooks.get_one(self.session, id)
|
||||
|
||||
def get_all(self) -> list[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)}
|
||||
)
|
||||
|
||||
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 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
|
||||
@@ -14,7 +14,7 @@ from mealie.services.events import create_recipe_event
|
||||
logger = get_logger(module=__name__)
|
||||
|
||||
|
||||
class RecipeService(BaseHttpService[str, str]):
|
||||
class RecipeService(BaseHttpService[str, Recipe]):
|
||||
"""
|
||||
Class Methods:
|
||||
`read_existing`: Reads an existing recipe from the database.
|
||||
@@ -23,7 +23,6 @@ class RecipeService(BaseHttpService[str, str]):
|
||||
"""
|
||||
|
||||
event_func = create_recipe_event
|
||||
recipe: Recipe # Required for proper type hints
|
||||
|
||||
@classmethod
|
||||
def write_existing(cls, slug: str, deps: WriteDeps = Depends()):
|
||||
@@ -34,17 +33,17 @@ class RecipeService(BaseHttpService[str, str]):
|
||||
return super().write_existing(slug, deps)
|
||||
|
||||
def assert_existing(self, slug: str):
|
||||
self.pupulate_recipe(slug)
|
||||
self.populate_item(slug)
|
||||
|
||||
if not self.recipe:
|
||||
if not self.item:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if not self.recipe.settings.public and not self.user:
|
||||
if not self.item.settings.public and not self.user:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def pupulate_recipe(self, slug: str) -> Recipe:
|
||||
self.recipe = self.db.recipes.get(self.session, slug)
|
||||
return self.recipe
|
||||
def populate_item(self, slug: str) -> Recipe:
|
||||
self.item = self.db.recipes.get(self.session, slug)
|
||||
return self.item
|
||||
|
||||
# CRUD METHODS
|
||||
def create_recipe(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe:
|
||||
@@ -52,34 +51,34 @@ class RecipeService(BaseHttpService[str, str]):
|
||||
create_data = Recipe(name=create_data.name)
|
||||
|
||||
try:
|
||||
self.recipe = self.db.recipes.create(self.session, create_data)
|
||||
self.item = self.db.recipes.create(self.session, create_data)
|
||||
except IntegrityError:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "RECIPE_ALREADY_EXISTS"})
|
||||
|
||||
self._create_event(
|
||||
"Recipe Created (URL)",
|
||||
f"'{self.recipe.name}' by {self.user.username} \n {self.settings.BASE_URL}/recipe/{self.recipe.slug}",
|
||||
f"'{self.item.name}' by {self.user.username} \n {self.settings.BASE_URL}/recipe/{self.item.slug}",
|
||||
)
|
||||
|
||||
return self.recipe
|
||||
return self.item
|
||||
|
||||
def update_recipe(self, update_data: Recipe) -> Recipe:
|
||||
original_slug = self.recipe.slug
|
||||
original_slug = self.item.slug
|
||||
|
||||
try:
|
||||
self.recipe = self.db.recipes.update(self.session, original_slug, update_data)
|
||||
self.item = self.db.recipes.update(self.session, original_slug, update_data)
|
||||
except IntegrityError:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "RECIPE_ALREADY_EXISTS"})
|
||||
|
||||
self._check_assets(original_slug)
|
||||
|
||||
return self.recipe
|
||||
return self.item
|
||||
|
||||
def patch_recipe(self, patch_data: Recipe) -> Recipe:
|
||||
original_slug = self.recipe.slug
|
||||
original_slug = self.item.slug
|
||||
|
||||
try:
|
||||
self.recipe = self.db.recipes.patch(
|
||||
self.item = self.db.recipes.patch(
|
||||
self.session, original_slug, patch_data.dict(exclude_unset=True, exclude_defaults=True)
|
||||
)
|
||||
except IntegrityError:
|
||||
@@ -87,7 +86,7 @@ class RecipeService(BaseHttpService[str, str]):
|
||||
|
||||
self._check_assets(original_slug)
|
||||
|
||||
return self.recipe
|
||||
return self.item
|
||||
|
||||
def delete_recipe(self) -> Recipe:
|
||||
"""removes a recipe from the database and purges the existing files from the filesystem.
|
||||
@@ -100,7 +99,7 @@ class RecipeService(BaseHttpService[str, str]):
|
||||
"""
|
||||
|
||||
try:
|
||||
recipe: Recipe = self.db.recipes.delete(self.session, self.recipe.slug)
|
||||
recipe: Recipe = self.db.recipes.delete(self.session, self.item.slug)
|
||||
self._delete_assets()
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
@@ -110,18 +109,18 @@ class RecipeService(BaseHttpService[str, str]):
|
||||
|
||||
def _check_assets(self, original_slug: str) -> None:
|
||||
"""Checks if the recipe slug has changed, and if so moves the assets to a new file with the new slug."""
|
||||
if original_slug != self.recipe.slug:
|
||||
if original_slug != self.item.slug:
|
||||
current_dir = self.app_dirs.RECIPE_DATA_DIR.joinpath(original_slug)
|
||||
|
||||
try:
|
||||
copytree(current_dir, self.recipe.directory, dirs_exist_ok=True)
|
||||
logger.info(f"Renaming Recipe Directory: {original_slug} -> {self.recipe.slug}")
|
||||
copytree(current_dir, self.item.directory, dirs_exist_ok=True)
|
||||
logger.info(f"Renaming Recipe Directory: {original_slug} -> {self.item.slug}")
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Recipe Directory not Found: {original_slug}")
|
||||
|
||||
all_asset_files = [x.file_name for x in self.recipe.assets]
|
||||
all_asset_files = [x.file_name for x in self.item.assets]
|
||||
|
||||
for file in self.recipe.asset_dir.iterdir():
|
||||
for file in self.item.asset_dir.iterdir():
|
||||
file: Path
|
||||
if file.is_dir():
|
||||
continue
|
||||
@@ -129,6 +128,6 @@ class RecipeService(BaseHttpService[str, str]):
|
||||
file.unlink()
|
||||
|
||||
def _delete_assets(self) -> None:
|
||||
recipe_dir = self.recipe.directory
|
||||
recipe_dir = self.item.directory
|
||||
rmtree(recipe_dir, ignore_errors=True)
|
||||
logger.info(f"Recipe Directory Removed: {self.recipe.slug}")
|
||||
logger.info(f"Recipe Directory Removed: {self.item.slug}")
|
||||
|
||||
Reference in New Issue
Block a user