mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-06-12 20:10:14 -04:00
Refactor/conver to controllers (#923)
* add dependency injection for get_repositories * convert events api to controller * update generic typing * add abstract controllers * update test naming * migrate admin services to controllers * add additional admin route tests * remove print * add public shared dependencies * add types * fix typo * add static variables for recipe json keys * add coverage gutters config * update controller routers * add generic success response * add category/tag/tool tests * add token refresh test * add coverage utilities * covert comments to controller * add todo * add helper properties * delete old service * update test notes * add unit test for pretty_stats * remove dead code from post_webhooks * update group routes to use controllers * add additional group test coverage * abstract common permission checks * convert ingredient parser to controller * update recipe crud to use controller * remove dead-code * add class lifespan tracker for debugging * convert bulk export to controller * migrate tools router to controller * update recipe share to controller * move customer router to _base * ignore prints in flake8 * convert units and foods to new controllers * migrate user routes to controllers * centralize error handling * fix invalid ref * reorder fields * update routers to share common handling * update tests * remove prints * fix cookbooks delete * fix cookbook get * add controller for mealplanner * cover report routes to controller * remove __future__ imports * remove dead code * remove all base_http children and remove dead code
This commit is contained in:
4
mealie/routes/_base/__init__.py
Normal file
4
mealie/routes/_base/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .abc_controller import *
|
||||
from .controller import *
|
||||
from .dependencies import *
|
||||
from .mixins import *
|
||||
58
mealie/routes/_base/abc_controller.py
Normal file
58
mealie/routes/_base/abc_controller.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from abc import ABC
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
from mealie.repos.all_repositories import AllRepositories
|
||||
from mealie.routes._base.checks import OperationChecks
|
||||
from mealie.routes._base.dependencies import SharedDependencies
|
||||
|
||||
|
||||
class BasePublicController(ABC):
|
||||
"""
|
||||
This is a public class for all User restricted controllers in the API.
|
||||
It includes the common SharedDependencies and some common methods used
|
||||
by all Admin controllers.
|
||||
"""
|
||||
|
||||
deps: SharedDependencies = Depends(SharedDependencies.public)
|
||||
|
||||
|
||||
class BaseUserController(ABC):
|
||||
"""
|
||||
This is a base class for all User restricted controllers in the API.
|
||||
It includes the common SharedDependencies and some common methods used
|
||||
by all Admin controllers.
|
||||
"""
|
||||
|
||||
deps: SharedDependencies = Depends(SharedDependencies.user)
|
||||
|
||||
@cached_property
|
||||
def repos(self):
|
||||
return AllRepositories(self.deps.session)
|
||||
|
||||
@property
|
||||
def group_id(self):
|
||||
return self.deps.acting_user.group_id
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self.deps.acting_user
|
||||
|
||||
@property
|
||||
def group(self):
|
||||
return self.deps.repos.groups.get_one(self.group_id)
|
||||
|
||||
@cached_property
|
||||
def checks(self) -> OperationChecks:
|
||||
return OperationChecks(self.deps.acting_user)
|
||||
|
||||
|
||||
class BaseAdminController(BaseUserController):
|
||||
"""
|
||||
This is a base class for all Admin restricted controllers in the API.
|
||||
It includes the common Shared Dependencies and some common methods used
|
||||
by all Admin controllers.
|
||||
"""
|
||||
|
||||
deps: SharedDependencies = Depends(SharedDependencies.admin)
|
||||
39
mealie/routes/_base/checks.py
Normal file
39
mealie/routes/_base/checks.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
|
||||
|
||||
class OperationChecks:
|
||||
"""
|
||||
OperationChecks class is a mixin class that can be used on routers to provide common permission
|
||||
checks and raise the appropriate http error as necessary
|
||||
"""
|
||||
|
||||
user: PrivateUser
|
||||
|
||||
def __init__(self, user: PrivateUser) -> None:
|
||||
self.user = user
|
||||
|
||||
def _raise_unauthorized(self) -> None:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def _raise_forbidden(self) -> None:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# =========================================
|
||||
# User Permission Checks
|
||||
|
||||
def can_manage(self) -> bool:
|
||||
if not self.user.can_manage:
|
||||
self._raise_forbidden()
|
||||
return True
|
||||
|
||||
def can_invite(self) -> bool:
|
||||
if not self.user.can_invite:
|
||||
self._raise_forbidden()
|
||||
return True
|
||||
|
||||
def can_organize(self) -> bool:
|
||||
if not self.user.can_organize:
|
||||
self._raise_forbidden()
|
||||
return True
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import cached_property
|
||||
from logging import Logger
|
||||
|
||||
@@ -17,34 +15,40 @@ from mealie.repos import AllRepositories
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
|
||||
|
||||
def _get_logger() -> Logger:
|
||||
return get_logger()
|
||||
|
||||
|
||||
class SharedDependencies:
|
||||
session: Session
|
||||
t: AbstractLocaleProvider
|
||||
logger: Logger
|
||||
acting_user: PrivateUser | None
|
||||
|
||||
def __init__(self, session: Session, acting_user: PrivateUser | None) -> None:
|
||||
self.t = get_locale_provider()
|
||||
self.logger = _get_logger()
|
||||
self.session = session
|
||||
self.acting_user = acting_user
|
||||
|
||||
@classmethod
|
||||
def public(cls, session: Session = Depends(generate_session)) -> "SharedDependencies":
|
||||
return cls(session, None)
|
||||
|
||||
@classmethod
|
||||
def user(
|
||||
cls, session: Session = Depends(generate_session), user: PrivateUser = Depends(get_current_user)
|
||||
cls,
|
||||
session: Session = Depends(generate_session),
|
||||
user: PrivateUser = Depends(get_current_user),
|
||||
) -> "SharedDependencies":
|
||||
return cls(session, user)
|
||||
|
||||
@classmethod
|
||||
def admin(
|
||||
cls, session: Session = Depends(generate_session), admin: PrivateUser = Depends(get_admin_user)
|
||||
cls,
|
||||
session: Session = Depends(generate_session),
|
||||
admin: PrivateUser = Depends(get_admin_user),
|
||||
) -> "SharedDependencies":
|
||||
return cls(session, admin)
|
||||
|
||||
@cached_property
|
||||
def logger(self) -> Logger:
|
||||
return get_logger()
|
||||
|
||||
@cached_property
|
||||
def settings(self) -> AppSettings:
|
||||
return get_app_settings()
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from logging import Logger
|
||||
from typing import Callable, Type
|
||||
from typing import Callable, Generic, Type, TypeVar
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from pydantic import UUID4, BaseModel
|
||||
|
||||
from mealie.repos.repository_generic import RepositoryGeneric
|
||||
from mealie.schema.response import ErrorResponse
|
||||
|
||||
C = TypeVar("C", bound=BaseModel)
|
||||
R = TypeVar("R", bound=BaseModel)
|
||||
U = TypeVar("U", bound=BaseModel)
|
||||
|
||||
|
||||
class CrudMixins(Generic[C, R, U]):
|
||||
"""
|
||||
The CrudMixins[C, R, U] class is a mixin class that provides a common set of methods for CRUD operations.
|
||||
This class is inteded to be used in a composition pattern where a class has a mixin property. For example:
|
||||
|
||||
```
|
||||
class MyClass:
|
||||
def __init(self repo, logger):
|
||||
self.mixins = CrudMixins(repo, logger)
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
class CrudMixins:
|
||||
repo: RepositoryGeneric
|
||||
exception_msgs: Callable[[Type[Exception]], str] | None
|
||||
default_message: str = "An unexpected error occurred."
|
||||
@@ -21,17 +36,7 @@ class CrudMixins:
|
||||
exception_msgs: Callable[[Type[Exception]], str] = None,
|
||||
default_message: str = None,
|
||||
) -> None:
|
||||
"""
|
||||
The CrudMixins class is a mixin class that provides a common set of methods for CRUD operations.
|
||||
This class is inteded to be used in a composition pattern where a class has a mixin property. For example:
|
||||
|
||||
```
|
||||
class MyClass:
|
||||
def __init(self repo, logger):
|
||||
self.mixins = CrudMixins(repo, logger)
|
||||
```
|
||||
|
||||
"""
|
||||
self.repo = repo
|
||||
self.logger = logger
|
||||
self.exception_msgs = exception_msgs
|
||||
@@ -39,16 +44,6 @@ class CrudMixins:
|
||||
if default_message:
|
||||
self.default_message = default_message
|
||||
|
||||
def set_default_message(self, default_msg: str) -> "CrudMixins":
|
||||
"""
|
||||
Use this method to set a lookup function for exception messages. When an exception is raised, and
|
||||
no custom message is set, the default message will be used.
|
||||
|
||||
IMPORTANT! The function returns the same instance of the CrudMixins class, so you can chain calls.
|
||||
"""
|
||||
self.default_msg = default_msg
|
||||
return self
|
||||
|
||||
def get_exception_message(self, ext: Exception) -> str:
|
||||
if self.exception_msgs:
|
||||
return self.exception_msgs(type(ext))
|
||||
@@ -67,8 +62,8 @@ class CrudMixins:
|
||||
detail=ErrorResponse.respond(message=msg, exception=str(ex)),
|
||||
)
|
||||
|
||||
def create_one(self, data):
|
||||
item = None
|
||||
def create_one(self, data: C) -> R | None:
|
||||
item: R | None = None
|
||||
try:
|
||||
item = self.repo.create(data)
|
||||
except Exception as ex:
|
||||
@@ -76,8 +71,8 @@ class CrudMixins:
|
||||
|
||||
return item
|
||||
|
||||
def get_one(self, item_id):
|
||||
item = self.repo.get(item_id)
|
||||
def get_one(self, item_id: int | str | UUID4, key: str = None) -> R:
|
||||
item = self.repo.get_one(item_id, key)
|
||||
|
||||
if not item:
|
||||
raise HTTPException(
|
||||
@@ -87,33 +82,35 @@ class CrudMixins:
|
||||
|
||||
return item
|
||||
|
||||
def update_one(self, data, item_id):
|
||||
item = self.repo.get(item_id)
|
||||
def update_one(self, data: U, item_id: int | str | UUID4) -> R:
|
||||
item: R = self.repo.get_one(item_id)
|
||||
|
||||
if not item:
|
||||
return
|
||||
raise HTTPException(
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
detail=ErrorResponse.respond(message="Not found."),
|
||||
)
|
||||
|
||||
try:
|
||||
item = self.repo.update(item.id, data) # type: ignore
|
||||
item = self.repo.update(item_id, data) # type: ignore
|
||||
except Exception as ex:
|
||||
self.handle_exception(ex)
|
||||
|
||||
return item
|
||||
|
||||
def patch_one(self, data, item_id) -> None:
|
||||
self.repo.get(item_id)
|
||||
def patch_one(self, data: U, item_id: int | str | UUID4) -> None:
|
||||
self.repo.get_one(item_id)
|
||||
|
||||
try:
|
||||
self.repo.patch(item_id, data.dict(exclude_unset=True, exclude_defaults=True))
|
||||
except Exception as ex:
|
||||
self.handle_exception(ex)
|
||||
|
||||
def delete_one(self, item_id):
|
||||
self.logger.info(f"Deleting item with id {item_id}")
|
||||
|
||||
def delete_one(self, item_id: int | str | UUID4) -> R | None:
|
||||
item: R | None = None
|
||||
try:
|
||||
item = self.repo.delete(item_id)
|
||||
self.logger.info(item)
|
||||
self.logger.info(f"Deleting item with id {item_id}")
|
||||
except Exception as ex:
|
||||
self.handle_exception(ex)
|
||||
|
||||
|
||||
@@ -1,35 +1,25 @@
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
from mealie.routes._base.routers import AdminAPIRouter
|
||||
from mealie.schema.events import EventsOut
|
||||
|
||||
from .._base import BaseAdminController, controller
|
||||
|
||||
router = AdminAPIRouter(prefix="/events")
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
@controller(router)
|
||||
class EventsController(BaseAdminController):
|
||||
@router.get("", response_model=EventsOut)
|
||||
async def get_events(self):
|
||||
"""Get event from the Database"""
|
||||
return EventsOut(total=self.repos.events.count_all(), events=self.repos.events.get_all(order_by="time_stamp"))
|
||||
|
||||
@router.get("", response_model=EventsOut)
|
||||
async def get_events(session: Session = Depends(generate_session)):
|
||||
"""Get event from the Database"""
|
||||
db = get_repositories(session)
|
||||
@router.delete("")
|
||||
async def delete_events(self):
|
||||
"""Get event from the Database"""
|
||||
self.repos.events.delete_all()
|
||||
return {"message": "All events deleted"}
|
||||
|
||||
return EventsOut(total=db.events.count_all(), events=db.events.get_all(order_by="time_stamp"))
|
||||
|
||||
|
||||
@router.delete("")
|
||||
async def delete_events(session: Session = Depends(generate_session)):
|
||||
"""Get event from the Database"""
|
||||
db = get_repositories(session)
|
||||
db.events.delete_all()
|
||||
return {"message": "All events deleted"}
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_event(id: int, session: Session = Depends(generate_session)):
|
||||
"""Delete event from the Database"""
|
||||
db = get_repositories(session)
|
||||
return db.events.delete(id)
|
||||
@router.delete("/{item_id}")
|
||||
async def delete_event(self, item_id: int):
|
||||
"""Delete event from the Database"""
|
||||
return self.repos.events.delete(item_id)
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
from mealie.services._base_http_service.router_factory import RouterFactory
|
||||
from mealie.services.admin.admin_group_service import AdminGroupService
|
||||
from mealie.services.admin.admin_user_service import AdminUserService
|
||||
from mealie.routes._base.routers import AdminAPIRouter
|
||||
|
||||
from . import admin_about, admin_email, admin_log, admin_server_tasks
|
||||
from . import admin_about, admin_email, admin_log, admin_management_groups, admin_management_users, admin_server_tasks
|
||||
|
||||
router = AdminAPIRouter(prefix="/admin")
|
||||
|
||||
router.include_router(admin_about.router, tags=["Admin: About"])
|
||||
router.include_router(admin_log.router, tags=["Admin: Log"])
|
||||
router.include_router(RouterFactory(AdminUserService, prefix="/users", tags=["Admin: Users"]))
|
||||
router.include_router(RouterFactory(AdminGroupService, prefix="/groups", tags=["Admin: Groups"]))
|
||||
router.include_router(admin_management_users.router)
|
||||
router.include_router(admin_management_groups.router)
|
||||
router.include_router(admin_email.router, tags=["Admin: Email"])
|
||||
router.include_router(admin_server_tasks.router, tags=["Admin: Server Tasks"])
|
||||
|
||||
@@ -1,55 +1,51 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm.session import Session
|
||||
from fastapi import APIRouter
|
||||
|
||||
from mealie.core.config import get_app_settings
|
||||
from mealie.core.release_checker import get_latest_version
|
||||
from mealie.core.settings.static import APP_VERSION
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes._base import BaseAdminController, controller
|
||||
from mealie.schema.admin.about import AdminAboutInfo, AppStatistics, CheckAppConfig
|
||||
|
||||
router = APIRouter(prefix="/about")
|
||||
|
||||
|
||||
@router.get("", response_model=AdminAboutInfo)
|
||||
async def get_app_info():
|
||||
"""Get general application information"""
|
||||
settings = get_app_settings()
|
||||
@controller(router)
|
||||
class AdminAboutController(BaseAdminController):
|
||||
@router.get("", response_model=AdminAboutInfo)
|
||||
async def get_app_info(self):
|
||||
"""Get general application information"""
|
||||
settings = self.deps.settings
|
||||
|
||||
return AdminAboutInfo(
|
||||
production=settings.PRODUCTION,
|
||||
version=APP_VERSION,
|
||||
versionLatest=get_latest_version(),
|
||||
demo_status=settings.IS_DEMO,
|
||||
api_port=settings.API_PORT,
|
||||
api_docs=settings.API_DOCS,
|
||||
db_type=settings.DB_ENGINE,
|
||||
db_url=settings.DB_URL_PUBLIC,
|
||||
default_group=settings.DEFAULT_GROUP,
|
||||
)
|
||||
return AdminAboutInfo(
|
||||
production=settings.PRODUCTION,
|
||||
version=APP_VERSION,
|
||||
versionLatest=get_latest_version(),
|
||||
demo_status=settings.IS_DEMO,
|
||||
api_port=settings.API_PORT,
|
||||
api_docs=settings.API_DOCS,
|
||||
db_type=settings.DB_ENGINE,
|
||||
db_url=settings.DB_URL_PUBLIC,
|
||||
default_group=settings.DEFAULT_GROUP,
|
||||
)
|
||||
|
||||
@router.get("/statistics", response_model=AppStatistics)
|
||||
async def get_app_statistics(self):
|
||||
|
||||
@router.get("/statistics", response_model=AppStatistics)
|
||||
async def get_app_statistics(session: Session = Depends(generate_session)):
|
||||
db = get_repositories(session)
|
||||
return AppStatistics(
|
||||
total_recipes=db.recipes.count_all(),
|
||||
uncategorized_recipes=db.recipes.count_uncategorized(),
|
||||
untagged_recipes=db.recipes.count_untagged(),
|
||||
total_users=db.users.count_all(),
|
||||
total_groups=db.groups.count_all(),
|
||||
)
|
||||
return AppStatistics(
|
||||
total_recipes=self.repos.recipes.count_all(),
|
||||
uncategorized_recipes=self.repos.recipes.count_uncategorized(),
|
||||
untagged_recipes=self.repos.recipes.count_untagged(),
|
||||
total_users=self.repos.users.count_all(),
|
||||
total_groups=self.repos.groups.count_all(),
|
||||
)
|
||||
|
||||
@router.get("/check", response_model=CheckAppConfig)
|
||||
async def check_app_config(self):
|
||||
settings = self.deps.settings
|
||||
url_set = settings.BASE_URL != "http://localhost:8080"
|
||||
|
||||
@router.get("/check", response_model=CheckAppConfig)
|
||||
async def check_app_config():
|
||||
settings = get_app_settings()
|
||||
|
||||
url_set = settings.BASE_URL != "http://localhost:8080"
|
||||
|
||||
return CheckAppConfig(
|
||||
email_ready=settings.SMTP_ENABLE,
|
||||
ldap_ready=settings.LDAP_ENABLED,
|
||||
base_url_set=url_set,
|
||||
is_up_to_date=get_latest_version() == APP_VERSION,
|
||||
)
|
||||
return CheckAppConfig(
|
||||
email_ready=settings.SMTP_ENABLE,
|
||||
ldap_ready=settings.LDAP_ENABLED,
|
||||
base_url_set=url_set,
|
||||
is_up_to_date=get_latest_version() == APP_VERSION,
|
||||
)
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi_camelcase import CamelModel
|
||||
|
||||
from mealie.core.config import get_app_settings
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.routes._base import BaseAdminController, controller
|
||||
from mealie.services.email import EmailService
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/email")
|
||||
|
||||
|
||||
@@ -23,24 +20,23 @@ class EmailTest(CamelModel):
|
||||
email: str
|
||||
|
||||
|
||||
@router.get("", response_model=EmailReady)
|
||||
async def check_email_config():
|
||||
"""Get general application information"""
|
||||
settings = get_app_settings()
|
||||
@controller(router)
|
||||
class AdminEmailController(BaseAdminController):
|
||||
@router.get("", response_model=EmailReady)
|
||||
async def check_email_config(self):
|
||||
"""Get general application information"""
|
||||
return EmailReady(ready=self.deps.settings.SMTP_ENABLE)
|
||||
|
||||
return EmailReady(ready=settings.SMTP_ENABLE)
|
||||
@router.post("", response_model=EmailSuccess)
|
||||
async def send_test_email(self, data: EmailTest):
|
||||
service = EmailService()
|
||||
status = False
|
||||
error = None
|
||||
|
||||
try:
|
||||
status = service.send_test_email(data.email)
|
||||
except Exception as e:
|
||||
self.deps.logger.error(e)
|
||||
error = str(e)
|
||||
|
||||
@router.post("", response_model=EmailSuccess)
|
||||
async def send_test_email(data: EmailTest):
|
||||
service = EmailService()
|
||||
status = False
|
||||
error = None
|
||||
|
||||
try:
|
||||
status = service.send_test_email(data.email)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
error = str(e)
|
||||
|
||||
return EmailSuccess(success=status, error=error)
|
||||
return EmailSuccess(success=status, error=error)
|
||||
|
||||
88
mealie/routes/admin/admin_management_groups.py
Normal file
88
mealie/routes/admin/admin_management_groups.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.schema.group.group import GroupAdminUpdate
|
||||
from mealie.schema.mapper import mapper
|
||||
from mealie.schema.query import GetAll
|
||||
from mealie.schema.response.responses import ErrorResponse
|
||||
from mealie.schema.user.user import GroupBase, GroupInDB
|
||||
|
||||
from .._base import BaseAdminController, controller
|
||||
from .._base.dependencies import SharedDependencies
|
||||
from .._base.mixins import CrudMixins
|
||||
|
||||
router = APIRouter(prefix="/groups", tags=["Admin: Groups"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class AdminUserManagementRoutes(BaseAdminController):
|
||||
deps: SharedDependencies = Depends(SharedDependencies.user)
|
||||
|
||||
@cached_property
|
||||
def repo(self):
|
||||
if not self.deps.acting_user:
|
||||
raise Exception("No user is logged in.")
|
||||
|
||||
return self.deps.repos.groups
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
# =======================================================================
|
||||
# CRUD Operations
|
||||
|
||||
@property
|
||||
def mixins(self):
|
||||
return CrudMixins[GroupBase, GroupInDB, GroupAdminUpdate](
|
||||
self.repo,
|
||||
self.deps.logger,
|
||||
self.registered_exceptions,
|
||||
)
|
||||
|
||||
@router.get("", response_model=list[GroupInDB])
|
||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
||||
return self.repo.get_all(start=q.start, limit=q.limit, override_schema=GroupInDB)
|
||||
|
||||
@router.post("", response_model=GroupInDB, status_code=status.HTTP_201_CREATED)
|
||||
def create_one(self, data: GroupBase):
|
||||
return self.mixins.create_one(data)
|
||||
|
||||
@router.get("/{item_id}", response_model=GroupInDB)
|
||||
def get_one(self, item_id: UUID4):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.put("/{item_id}", response_model=GroupInDB)
|
||||
def update_one(self, item_id: UUID4, data: GroupAdminUpdate):
|
||||
group = self.repo.get_one(item_id)
|
||||
|
||||
if data.preferences:
|
||||
preferences = self.repos.group_preferences.get_one(value=item_id, key="group_id")
|
||||
preferences = mapper(data.preferences, preferences)
|
||||
group.preferences = self.repos.group_preferences.update(item_id, preferences)
|
||||
|
||||
if data.name not in ["", group.name]:
|
||||
group.name = data.name
|
||||
group = self.repo.update(item_id, group)
|
||||
|
||||
return group
|
||||
|
||||
@router.delete("/{item_id}", response_model=GroupInDB)
|
||||
def delete_one(self, item_id: UUID4):
|
||||
item = self.repo.get_one(item_id)
|
||||
|
||||
if len(item.users) > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorResponse.respond(message="Cannot delete group with users"),
|
||||
)
|
||||
|
||||
return self.mixins.delete_one(item_id)
|
||||
61
mealie/routes/admin/admin_management_users.py
Normal file
61
mealie/routes/admin/admin_management_users.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base import BaseAdminController, controller
|
||||
from mealie.routes._base.dependencies import SharedDependencies
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema.query import GetAll
|
||||
from mealie.schema.user.user import UserIn, UserOut
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["Admin: Users"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class AdminUserManagementRoutes(BaseAdminController):
|
||||
deps: SharedDependencies = Depends(SharedDependencies.user)
|
||||
|
||||
@cached_property
|
||||
def repo(self):
|
||||
if not self.deps.acting_user:
|
||||
raise Exception("No user is logged in.")
|
||||
|
||||
return self.deps.repos.users
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
# =======================================================================
|
||||
# CRUD Operations
|
||||
|
||||
@property
|
||||
def mixins(self):
|
||||
return CrudMixins[UserIn, UserOut, UserOut](self.repo, self.deps.logger, self.registered_exceptions)
|
||||
|
||||
@router.get("", response_model=list[UserOut])
|
||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
||||
return self.repo.get_all(start=q.start, limit=q.limit, override_schema=UserOut)
|
||||
|
||||
@router.post("", response_model=UserOut)
|
||||
def create_one(self, data: UserIn):
|
||||
return self.mixins.create_one(data)
|
||||
|
||||
@router.get("/{item_id}", response_model=UserOut)
|
||||
def get_one(self, item_id: UUID4):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.put("/{item_id}", response_model=UserOut)
|
||||
def update_one(self, item_id: UUID4, data: UserOut):
|
||||
return self.mixins.update_one(data, item_id)
|
||||
|
||||
@router.delete("/{item_id}", response_model=UserOut)
|
||||
def delete_one(self, item_id: UUID4):
|
||||
return self.mixins.delete_one(item_id)
|
||||
@@ -1,18 +1,20 @@
|
||||
from fastapi import Depends
|
||||
from fastapi import BackgroundTasks
|
||||
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.routes._base import BaseAdminController, controller
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.schema.server.tasks import ServerTask, ServerTaskNames
|
||||
from mealie.services.server_tasks import BackgroundExecutor, test_executor_func
|
||||
from mealie.services.server_tasks.tasks_http_service import AdminServerTasks
|
||||
|
||||
router = UserAPIRouter()
|
||||
|
||||
|
||||
@router.get("/server-tasks", response_model=list[ServerTask])
|
||||
def get_all_tasks(tasks_service: AdminServerTasks = Depends(AdminServerTasks.private)):
|
||||
return tasks_service.get_all()
|
||||
@controller(router)
|
||||
class AdminServerTasksController(BaseAdminController):
|
||||
@router.get("/server-tasks", response_model=list[ServerTask])
|
||||
def get_all(self):
|
||||
return self.repos.server_tasks.get_all(order_by="created_at")
|
||||
|
||||
|
||||
@router.post("/server-tasks", response_model=ServerTask)
|
||||
def create_test_tasks(bg_executor: BackgroundExecutor = Depends(BackgroundExecutor.private)):
|
||||
return bg_executor.dispatch(ServerTaskNames.default, test_executor_func)
|
||||
@router.post("/server-tasks", response_model=ServerTask, status_code=201)
|
||||
def create_test_tasks(self, bg_tasks: BackgroundTasks):
|
||||
bg_executor = BackgroundExecutor(self.group.id, self.repos, bg_tasks)
|
||||
return bg_executor.dispatch(ServerTaskNames.default, test_executor_func)
|
||||
|
||||
@@ -4,13 +4,14 @@ from typing import Optional
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Form, Request, status
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core import security
|
||||
from mealie.core.dependencies import get_current_user
|
||||
from mealie.core.security import authenticate_user
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.schema.user import PrivateUser
|
||||
from mealie.services.events import create_user_event
|
||||
|
||||
@@ -38,6 +39,15 @@ class CustomOAuth2Form(OAuth2PasswordRequestForm):
|
||||
self.client_secret = client_secret
|
||||
|
||||
|
||||
class MealieAuthToken(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
@classmethod
|
||||
def respond(cls, token: str, token_type: str = "bearer") -> dict:
|
||||
return cls(access_token=token, token_type=token_type).dict()
|
||||
|
||||
|
||||
@public_router.post("/token")
|
||||
def get_token(
|
||||
background_tasks: BackgroundTasks,
|
||||
@@ -61,11 +71,11 @@ def get_token(
|
||||
|
||||
duration = timedelta(days=14) if data.remember_me else None
|
||||
access_token = security.create_access_token(dict(sub=user.email), duration)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
return MealieAuthToken.respond(access_token)
|
||||
|
||||
|
||||
@user_router.get("/refresh")
|
||||
async def refresh_token(current_user: PrivateUser = Depends(get_current_user)):
|
||||
"""Use a valid token to get another token"""
|
||||
access_token = security.create_access_token(data=dict(sub=current_user.email))
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
return MealieAuthToken.respond(access_token)
|
||||
|
||||
@@ -10,7 +10,7 @@ from mealie.core.dependencies import get_current_user
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.core.security import create_file_token
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
from mealie.routes._base.routers import AdminAPIRouter
|
||||
from mealie.schema.admin import AllBackups, BackupFile, CreateBackup, ImportJob
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
from mealie.services.backups import imports
|
||||
|
||||
@@ -2,10 +2,5 @@ from fastapi import APIRouter
|
||||
|
||||
from . import categories
|
||||
|
||||
prefix = "/categories"
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(categories.public_router, prefix=prefix, tags=["Categories: CRUD"])
|
||||
router.include_router(categories.user_router, prefix=prefix, tags=["Categories: CRUD"])
|
||||
router.include_router(categories.admin_router, prefix=prefix, tags=["Categories: CRUD"])
|
||||
router.include_router(categories.router)
|
||||
|
||||
@@ -1,82 +1,68 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm.session import Session
|
||||
from functools import cached_property
|
||||
|
||||
from mealie.core.dependencies import is_logged_in
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse
|
||||
from mealie.schema.recipe.recipe_category import CategoryBase
|
||||
|
||||
public_router = APIRouter()
|
||||
user_router = UserAPIRouter()
|
||||
admin_router = AdminAPIRouter()
|
||||
logger = get_logger()
|
||||
router = APIRouter(prefix="/categories", tags=["Categories: CRUD"])
|
||||
|
||||
|
||||
@public_router.get("")
|
||||
async def get_all_recipe_categories(session: Session = Depends(generate_session)):
|
||||
"""Returns a list of available categories in the database"""
|
||||
db = get_repositories(session)
|
||||
return db.categories.get_all_limit_columns(fields=["slug", "name"])
|
||||
class CategorySummary(BaseModel):
|
||||
slug: str
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
@public_router.get("/empty")
|
||||
def get_empty_categories(session: Session = Depends(generate_session)):
|
||||
"""Returns a list of categories that do not contain any recipes"""
|
||||
db = get_repositories(session)
|
||||
return db.categories.get_empty()
|
||||
@controller(router)
|
||||
class RecipeCategoryController(BaseUserController):
|
||||
# =========================================================================
|
||||
# CRUD Operations
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return CrudMixins(self.repos.categories, self.deps.logger)
|
||||
|
||||
@public_router.get("/{category}", response_model=RecipeCategoryResponse)
|
||||
def get_all_recipes_by_category(
|
||||
category: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
|
||||
):
|
||||
"""Returns a list of recipes associated with the provided category."""
|
||||
db = get_repositories(session)
|
||||
@router.get("", response_model=list[CategorySummary])
|
||||
def get_all(self):
|
||||
"""Returns a list of available categories in the database"""
|
||||
return self.repos.categories.get_all_limit_columns(fields=["slug", "name"])
|
||||
|
||||
category_obj = db.categories.get(category)
|
||||
category_obj = RecipeCategoryResponse.from_orm(category_obj)
|
||||
@router.post("", status_code=201)
|
||||
def create_one(self, category: CategoryIn):
|
||||
"""Creates a Category in the database"""
|
||||
return self.mixins.create_one(category)
|
||||
|
||||
if not is_user:
|
||||
category_obj.recipes = [x for x in category_obj.recipes if x.settings.public]
|
||||
@router.get("/{slug}", response_model=RecipeCategoryResponse)
|
||||
def get_all_recipes_by_category(self, slug: str):
|
||||
"""Returns a list of recipes associated with the provided category."""
|
||||
category_obj = self.repos.categories.get(slug)
|
||||
category_obj = RecipeCategoryResponse.from_orm(category_obj)
|
||||
return category_obj
|
||||
|
||||
return category_obj
|
||||
@router.put("/{slug}", response_model=RecipeCategoryResponse)
|
||||
def update_one(self, slug: str, update_data: CategoryIn):
|
||||
"""Updates an existing Tag in the database"""
|
||||
return self.mixins.update_one(update_data, slug)
|
||||
|
||||
@router.delete("/{slug}")
|
||||
def delete_one(self, slug: str):
|
||||
"""
|
||||
Removes a recipe category from the database. Deleting a
|
||||
category does not impact a recipe. The category will be removed
|
||||
from any recipes that contain it
|
||||
"""
|
||||
self.mixins.delete_one(slug)
|
||||
|
||||
@user_router.post("")
|
||||
async def create_recipe_category(category: CategoryIn, session: Session = Depends(generate_session)):
|
||||
"""Creates a Category in the database"""
|
||||
db = get_repositories(session)
|
||||
# =========================================================================
|
||||
# Read All Operations
|
||||
|
||||
try:
|
||||
return db.categories.create(category.dict())
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@admin_router.put("/{category}", response_model=RecipeCategoryResponse)
|
||||
async def update_recipe_category(category: str, new_category: CategoryIn, session: Session = Depends(generate_session)):
|
||||
"""Updates an existing Tag in the database"""
|
||||
db = get_repositories(session)
|
||||
|
||||
try:
|
||||
return db.categories.update(category, new_category.dict())
|
||||
except Exception:
|
||||
logger.exception("Failed to update category")
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@admin_router.delete("/{category}")
|
||||
async def delete_recipe_category(category: str, session: Session = Depends(generate_session)):
|
||||
"""
|
||||
Removes a recipe category from the database. Deleting a
|
||||
category does not impact a recipe. The category will be removed
|
||||
from any recipes that contain it
|
||||
"""
|
||||
db = get_repositories(session)
|
||||
|
||||
try:
|
||||
db.categories.delete(category)
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
@router.get("/empty", response_model=list[CategoryBase])
|
||||
def get_all_empty(self):
|
||||
"""Returns a list of categories that do not contain any recipes"""
|
||||
return self.repos.categories.get_empty()
|
||||
|
||||
@@ -1,8 +1,74 @@
|
||||
from fastapi import APIRouter
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from mealie.services._base_http_service.router_factory import RouterFactory
|
||||
from mealie.services.recipe.recipe_comments_service import RecipeCommentsService
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import UUID4
|
||||
|
||||
router = APIRouter()
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema.query import GetAll
|
||||
from mealie.schema.recipe.recipe_comments import (
|
||||
RecipeCommentCreate,
|
||||
RecipeCommentOut,
|
||||
RecipeCommentSave,
|
||||
RecipeCommentUpdate,
|
||||
)
|
||||
from mealie.schema.response.responses import ErrorResponse, SuccessResponse
|
||||
|
||||
router.include_router(RouterFactory(RecipeCommentsService, prefix="/comments", tags=["Recipe: Comments"]))
|
||||
router = APIRouter(prefix="/comments", tags=["Recipe: Comments"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class RecipeCommentRoutes(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.deps.repos.comments
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
# =======================================================================
|
||||
# CRUD Operations
|
||||
|
||||
@property
|
||||
def mixins(self) -> CrudMixins:
|
||||
return CrudMixins(self.repo, self.deps.logger, self.registered_exceptions, "An unexpected error occurred.")
|
||||
|
||||
def _check_comment_belongs_to_user(self, item_id: UUID4) -> None:
|
||||
comment = self.repo.get_one(item_id)
|
||||
if comment.user_id != self.deps.acting_user.id and not self.deps.acting_user.admin:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=ErrorResponse.response(message="Comment does not belong to user"),
|
||||
)
|
||||
|
||||
@router.get("", response_model=list[RecipeCommentOut])
|
||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
||||
return self.repo.get_all(start=q.start, limit=q.limit, override_schema=RecipeCommentOut)
|
||||
|
||||
@router.post("", response_model=RecipeCommentOut, status_code=201)
|
||||
def create_one(self, data: RecipeCommentCreate):
|
||||
save_data = RecipeCommentSave(text=data.text, user_id=self.deps.acting_user.id, recipe_id=data.recipe_id)
|
||||
return self.mixins.create_one(save_data)
|
||||
|
||||
@router.get("/{item_id}", response_model=RecipeCommentOut)
|
||||
def get_one(self, item_id: UUID4):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.put("/{item_id}", response_model=RecipeCommentOut)
|
||||
def update_one(self, item_id: UUID4, data: RecipeCommentUpdate):
|
||||
self._check_comment_belongs_to_user(item_id)
|
||||
return self.mixins.update_one(data, item_id)
|
||||
|
||||
@router.delete("/{item_id}", response_model=SuccessResponse)
|
||||
def delete_one(self, item_id: UUID4):
|
||||
self._check_comment_belongs_to_user(item_id)
|
||||
self.mixins.delete_one(item_id)
|
||||
return SuccessResponse.respond(message="Comment deleted")
|
||||
|
||||
@@ -1,59 +1,29 @@
|
||||
from datetime import date, timedelta
|
||||
from fastapi import APIRouter
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from mealie.schema.reports.reports import ReportCategory
|
||||
from mealie.services._base_http_service import RouterFactory
|
||||
from mealie.services.group_services import CookbookService, WebhookService
|
||||
from mealie.services.group_services.meal_service import MealService
|
||||
from mealie.services.group_services.reports_service import GroupReportService
|
||||
|
||||
from . import categories, invitations, labels, migrations, notifications, preferences, self_service, shopping_lists
|
||||
from . import (
|
||||
controller_cookbooks,
|
||||
controller_group_notifications,
|
||||
controller_group_reports,
|
||||
controller_group_self_service,
|
||||
controller_invitations,
|
||||
controller_labels,
|
||||
controller_mealplan,
|
||||
controller_meaplan_config,
|
||||
controller_migrations,
|
||||
controller_shopping_lists,
|
||||
controller_webhooks,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(self_service.user_router)
|
||||
|
||||
|
||||
webhook_router = RouterFactory(service=WebhookService, prefix="/groups/webhooks", tags=["Groups: Webhooks"])
|
||||
cookbook_router = RouterFactory(service=CookbookService, prefix="/groups/cookbooks", tags=["Groups: Cookbooks"])
|
||||
|
||||
|
||||
@router.get("/groups/mealplans/today", tags=["Groups: Mealplans"])
|
||||
def get_todays_meals(ms: MealService = Depends(MealService.private)):
|
||||
return ms.get_today()
|
||||
|
||||
|
||||
meal_plan_router = RouterFactory(service=MealService, prefix="/groups/mealplans", tags=["Groups: Mealplans"])
|
||||
|
||||
|
||||
@meal_plan_router.get("")
|
||||
def get_all(start: date = None, limit: date = None, ms: MealService = Depends(MealService.private)):
|
||||
start = start or date.today() - timedelta(days=999)
|
||||
limit = limit or date.today() + timedelta(days=999)
|
||||
return ms.get_slice(start, limit)
|
||||
|
||||
|
||||
router.include_router(cookbook_router)
|
||||
router.include_router(meal_plan_router)
|
||||
router.include_router(categories.user_router)
|
||||
router.include_router(webhook_router)
|
||||
router.include_router(invitations.router, prefix="/groups/invitations", tags=["Groups: Invitations"])
|
||||
router.include_router(preferences.router, prefix="/groups/preferences", tags=["Group: Preferences"])
|
||||
router.include_router(migrations.router, prefix="/groups/migrations", tags=["Group: Migrations"])
|
||||
|
||||
report_router = RouterFactory(service=GroupReportService, prefix="/groups/reports", tags=["Groups: Reports"])
|
||||
|
||||
|
||||
@report_router.get("")
|
||||
def get_all_reports(
|
||||
report_type: ReportCategory = None,
|
||||
gs: GroupReportService = Depends(GroupReportService.private),
|
||||
):
|
||||
return gs._get_all(report_type)
|
||||
|
||||
|
||||
router.include_router(report_router)
|
||||
router.include_router(shopping_lists.router)
|
||||
router.include_router(labels.router)
|
||||
router.include_router(notifications.router)
|
||||
router.include_router(controller_group_self_service.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)
|
||||
router.include_router(controller_group_reports.router)
|
||||
router.include_router(controller_shopping_lists.router)
|
||||
router.include_router(controller_labels.router)
|
||||
router.include_router(controller_group_notifications.router)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
from fastapi import Depends
|
||||
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.recipe.recipe_category import CategoryBase
|
||||
from mealie.services.group_services.group_service import GroupSelfService
|
||||
|
||||
user_router = UserAPIRouter(prefix="/groups/categories", tags=["Groups: Mealplan Categories"])
|
||||
|
||||
|
||||
@user_router.get("", response_model=list[CategoryBase])
|
||||
def get_mealplan_categories(group_service: GroupSelfService = Depends(GroupSelfService.read_existing)):
|
||||
return group_service.item.categories
|
||||
|
||||
|
||||
@user_router.put("", response_model=list[CategoryBase])
|
||||
def update_mealplan_categories(
|
||||
new_categories: list[CategoryBase], group_service: GroupSelfService = Depends(GroupSelfService.write_existing)
|
||||
):
|
||||
|
||||
items = group_service.update_categories(new_categories)
|
||||
|
||||
return items.categories
|
||||
70
mealie/routes/groups/controller_cookbooks.py
Normal file
70
mealie/routes/groups/controller_cookbooks.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema import mapper
|
||||
from mealie.schema.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
|
||||
|
||||
router = APIRouter(prefix="/groups/cookbooks", tags=["Groups: Cookbooks"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class GroupCookbookController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.deps.repos.cookbooks.by_group(self.group_id)
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return CrudMixins[CreateCookBook, ReadCookBook, UpdateCookBook](
|
||||
self.repo,
|
||||
self.deps.logger,
|
||||
self.registered_exceptions,
|
||||
)
|
||||
|
||||
@router.get("", response_model=list[RecipeCookBook])
|
||||
def get_all(self):
|
||||
items = self.repo.get_all()
|
||||
items.sort(key=lambda x: x.position)
|
||||
return items
|
||||
|
||||
@router.post("", response_model=RecipeCookBook, status_code=201)
|
||||
def create_one(self, data: CreateCookBook):
|
||||
data = mapper.cast(data, SaveCookBook, group_id=self.group_id)
|
||||
return self.mixins.create_one(data)
|
||||
|
||||
@router.put("", response_model=list[ReadCookBook])
|
||||
def update_many(self, data: list[UpdateCookBook]):
|
||||
updated = []
|
||||
|
||||
for cookbook in data:
|
||||
cb = self.mixins.update_one(cookbook, cookbook.id)
|
||||
updated.append(cb)
|
||||
|
||||
return updated
|
||||
|
||||
@router.get("/{item_id}", response_model=RecipeCookBook)
|
||||
def get_one(self, item_id: str):
|
||||
try:
|
||||
item_id = int(item_id)
|
||||
return self.mixins.get_one(item_id)
|
||||
except Exception:
|
||||
self.mixins.get_one(item_id, key="slug")
|
||||
|
||||
@router.put("/{item_id}", response_model=RecipeCookBook)
|
||||
def update_one(self, item_id: int, data: CreateCookBook):
|
||||
return self.mixins.update_one(data, item_id)
|
||||
|
||||
@router.delete("/{item_id}", response_model=RecipeCookBook)
|
||||
def delete_one(self, item_id: int):
|
||||
return self.mixins.delete_one(item_id)
|
||||
@@ -1,10 +1,10 @@
|
||||
from functools import cached_property
|
||||
from sqlite3 import IntegrityError
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.dependencies import SharedDependencies
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
@@ -35,9 +35,9 @@ class GroupEventsNotifierController:
|
||||
return self.deps.repos.group_event_notifier.by_group(self.deps.acting_user.group_id)
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
Exception: "An unexpected error occurred.",
|
||||
IntegrityError: "An unexpected error occurred.",
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
45
mealie/routes/groups/controller_group_reports.py
Normal file
45
mealie/routes/groups/controller_group_reports.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema.reports.reports import ReportCategory, ReportCreate, ReportOut, ReportSummary
|
||||
|
||||
router = APIRouter(prefix="/groups/reports", tags=["Groups: Reports"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class GroupReportsController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.deps.repos.group_reports.by_group(self.deps.acting_user.group_id)
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
return {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}.get(ex, "An unexpected error occurred.")
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return CrudMixins[ReportCreate, ReportOut, ReportCreate](
|
||||
self.repo,
|
||||
self.deps.logger,
|
||||
self.registered_exceptions,
|
||||
)
|
||||
|
||||
@router.get("", response_model=list[ReportSummary])
|
||||
def get_all(self, report_type: ReportCategory = None):
|
||||
return self.repo.multi_query({"group_id": self.group_id, "category": report_type}, limit=9999)
|
||||
|
||||
@router.get("/{item_id}", response_model=ReportOut)
|
||||
def get_one(self, item_id: UUID4):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.delete("/{item_id}", status_code=204)
|
||||
def delete_one(self, item_id: UUID4):
|
||||
self.mixins.delete_one(item_id) # type: ignore
|
||||
49
mealie/routes/groups/controller_group_self_service.py
Normal file
49
mealie/routes/groups/controller_group_self_service.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.schema.group.group_permissions import SetPermissions
|
||||
from mealie.schema.group.group_preferences import ReadGroupPreferences, UpdateGroupPreferences
|
||||
from mealie.schema.user.user import GroupInDB, UserOut
|
||||
|
||||
router = UserAPIRouter(prefix="/groups", tags=["Groups: Self Service"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class GroupSelfServiceController(BaseUserController):
|
||||
@router.get("/preferences", response_model=ReadGroupPreferences)
|
||||
def get_group_preferences(self):
|
||||
return self.group.preferences
|
||||
|
||||
@router.put("/preferences", response_model=ReadGroupPreferences)
|
||||
def update_group_preferences(self, new_pref: UpdateGroupPreferences):
|
||||
return self.repos.group_preferences.update(self.group_id, new_pref)
|
||||
|
||||
@router.get("/self", response_model=GroupInDB)
|
||||
async def get_logged_in_user_group(self):
|
||||
"""Returns the Group Data for the Current User"""
|
||||
return self.group
|
||||
|
||||
@router.get("/members", response_model=list[UserOut])
|
||||
async def get_group_members(self):
|
||||
"""Returns the Group of user lists"""
|
||||
return self.repos.users.multi_query(query_by={"group_id": self.group.id}, override_schema=UserOut)
|
||||
|
||||
@router.put("/permissions", response_model=UserOut)
|
||||
async def set_member_permissions(self, permissions: SetPermissions):
|
||||
self.checks.can_manage()
|
||||
|
||||
target_user = self.repos.users.get(permissions.user_id)
|
||||
|
||||
if not target_user:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
|
||||
if target_user.group_id != self.group_id:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="User is not a member of this group")
|
||||
|
||||
target_user.can_invite = permissions.can_invite
|
||||
target_user.can_manage = permissions.can_manage
|
||||
target_user.can_organize = permissions.can_organize
|
||||
|
||||
return self.repos.users.update(permissions.user_id, target_user)
|
||||
43
mealie/routes/groups/controller_invitations.py
Normal file
43
mealie/routes/groups/controller_invitations.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
|
||||
from mealie.core.security import url_safe_token
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.schema.group.invite_token import (
|
||||
CreateInviteToken,
|
||||
EmailInitationResponse,
|
||||
EmailInvitation,
|
||||
ReadInviteToken,
|
||||
SaveInviteToken,
|
||||
)
|
||||
from mealie.services.email.email_service import EmailService
|
||||
|
||||
router = APIRouter(prefix="/groups/invitations", tags=["Groups: Invitations"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class GroupInvitationsController(BaseUserController):
|
||||
@router.get("", response_model=list[ReadInviteToken])
|
||||
def get_invite_tokens(self):
|
||||
return self.repos.group_invite_tokens.multi_query({"group_id": self.group_id})
|
||||
|
||||
@router.post("", response_model=ReadInviteToken, status_code=status.HTTP_201_CREATED)
|
||||
def create_invite_token(self, uses: CreateInviteToken):
|
||||
if not self.deps.acting_user.can_invite:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="User is not allowed to create invite tokens")
|
||||
|
||||
token = SaveInviteToken(uses_left=uses.uses, group_id=self.group_id, token=url_safe_token())
|
||||
return self.repos.group_invite_tokens.create(token)
|
||||
|
||||
@router.post("/email", response_model=EmailInitationResponse)
|
||||
def email_invitation(self, invite: EmailInvitation):
|
||||
email_service = EmailService()
|
||||
url = f"{self.deps.settings.BASE_URL}/register?token={invite.token}"
|
||||
|
||||
success = False
|
||||
error = None
|
||||
try:
|
||||
success = email_service.send_invitation(address=invite.email, invitation_url=url)
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
|
||||
return EmailInitationResponse(success=success, error=error)
|
||||
@@ -1,10 +1,10 @@
|
||||
from functools import cached_property
|
||||
from sqlite3 import IntegrityError
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.dependencies import SharedDependencies
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
@@ -17,15 +17,13 @@ from mealie.schema.labels import (
|
||||
)
|
||||
from mealie.schema.mapper import cast
|
||||
from mealie.schema.query import GetAll
|
||||
from mealie.services.group_services.shopping_lists import ShoppingListService
|
||||
|
||||
router = APIRouter(prefix="/groups/labels", tags=["Group: Multi Purpose Labels"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class ShoppingListRoutes:
|
||||
class MultiPurposeLabelsController:
|
||||
deps: SharedDependencies = Depends(SharedDependencies.user)
|
||||
service: ShoppingListService = Depends(ShoppingListService.private)
|
||||
|
||||
@cached_property
|
||||
def repo(self):
|
||||
@@ -35,9 +33,9 @@ class ShoppingListRoutes:
|
||||
return self.deps.repos.group_multi_purpose_labels.by_group(self.deps.acting_user.group_id)
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
Exception: "An unexpected error occurred.",
|
||||
IntegrityError: "An unexpected error occurred.",
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
62
mealie/routes/groups/controller_mealplan.py
Normal file
62
mealie/routes/groups/controller_mealplan.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from datetime import date, timedelta
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.repos.repository_meals import RepositoryMeals
|
||||
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
|
||||
|
||||
router = APIRouter(prefix="/groups/mealplans", tags=["Groups: Mealplans"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class GroupMealplanController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self) -> RepositoryMeals:
|
||||
return self.repos.meals.by_group(self.group_id)
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return CrudMixins[CreatePlanEntry, ReadPlanEntry, UpdatePlanEntry](
|
||||
self.repo,
|
||||
self.deps.logger,
|
||||
self.registered_exceptions,
|
||||
)
|
||||
|
||||
@router.get("/today", tags=["Groups: Mealplans"])
|
||||
def get_todays_meals(self):
|
||||
return self.repo.get_today(group_id=self.group_id)
|
||||
|
||||
@router.get("", response_model=list[ReadPlanEntry])
|
||||
def get_all(self, start: date = None, limit: date = None):
|
||||
start = start or date.today() - timedelta(days=999)
|
||||
limit = limit or date.today() + timedelta(days=999)
|
||||
return self.repo.get_slice(start, limit, group_id=self.group.id)
|
||||
|
||||
@router.post("", response_model=ReadPlanEntry, status_code=201)
|
||||
def create_one(self, data: CreatePlanEntry):
|
||||
data = mapper.cast(data, SavePlanEntry, group_id=self.group.id)
|
||||
return self.mixins.create_one(data)
|
||||
|
||||
@router.get("/{item_id}", response_model=ReadPlanEntry)
|
||||
def get_one(self, item_id: int):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.put("/{item_id}", response_model=ReadPlanEntry)
|
||||
def update_one(self, item_id: int, data: UpdatePlanEntry):
|
||||
return self.mixins.update_one(data, item_id)
|
||||
|
||||
@router.delete("/{item_id}", response_model=ReadPlanEntry)
|
||||
def delete_one(self, item_id: int):
|
||||
return self.mixins.delete_one(item_id)
|
||||
26
mealie/routes/groups/controller_meaplan_config.py
Normal file
26
mealie/routes/groups/controller_meaplan_config.py
Normal file
@@ -0,0 +1,26 @@
|
||||
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.recipe.recipe_category import CategoryBase
|
||||
from mealie.schema.user.user import GroupInDB
|
||||
|
||||
router = UserAPIRouter(prefix="/groups/categories", tags=["Groups: Mealplan Categories"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class GroupMealplanConfigController(BaseUserController):
|
||||
@property
|
||||
def mixins(self):
|
||||
return CrudMixins[GroupInDB, GroupInDB, GroupInDB](self.repos.groups, self.deps.logger)
|
||||
|
||||
@router.get("", response_model=list[CategoryBase])
|
||||
def get_mealplan_categories(self):
|
||||
data = self.mixins.get_one(self.deps.acting_user.group_id)
|
||||
return data.categories
|
||||
|
||||
@router.put("", response_model=list[CategoryBase])
|
||||
def update_mealplan_categories(self, new_categories: list[CategoryBase]):
|
||||
data = self.mixins.get_one(self.deps.acting_user.group_id)
|
||||
data.categories = new_categories
|
||||
return self.mixins.update_one(data, data.id).categories
|
||||
51
mealie/routes/groups/controller_migrations.py
Normal file
51
mealie/routes/groups/controller_migrations.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import shutil
|
||||
|
||||
from fastapi import Depends, File, Form
|
||||
from fastapi.datastructures import UploadFile
|
||||
|
||||
from mealie.core.dependencies import temporary_zip_path
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.schema.group.group_migration import SupportedMigrations
|
||||
from mealie.schema.reports.reports import ReportSummary
|
||||
from mealie.services.migrations import ChowdownMigrator, MealieAlphaMigrator, NextcloudMigrator, PaprikaMigrator
|
||||
|
||||
router = UserAPIRouter(prefix="/groups/migrations", tags=["Group: Migrations"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class GroupMigrationController(BaseUserController):
|
||||
@router.post("", response_model=ReportSummary)
|
||||
def start_data_migration(
|
||||
self,
|
||||
add_migration_tag: bool = Form(False),
|
||||
migration_type: SupportedMigrations = Form(...),
|
||||
archive: UploadFile = File(...),
|
||||
temp_path: str = Depends(temporary_zip_path),
|
||||
):
|
||||
# Save archive to temp_path
|
||||
with temp_path.open("wb") as buffer:
|
||||
shutil.copyfileobj(archive.file, buffer)
|
||||
|
||||
args = {
|
||||
"archive": temp_path,
|
||||
"db": self.repos,
|
||||
"session": self.deps.session,
|
||||
"user_id": self.user.id,
|
||||
"group_id": self.group_id,
|
||||
"add_migration_tag": add_migration_tag,
|
||||
}
|
||||
|
||||
match migration_type:
|
||||
case SupportedMigrations.chowdown:
|
||||
migrator = ChowdownMigrator(**args)
|
||||
case SupportedMigrations.mealie_alpha:
|
||||
migrator = MealieAlphaMigrator(**args)
|
||||
case SupportedMigrations.nextcloud:
|
||||
migrator = NextcloudMigrator(**args)
|
||||
case SupportedMigrations.paprika:
|
||||
migrator = PaprikaMigrator(**args)
|
||||
case _:
|
||||
raise ValueError(f"Unsupported migration type: {migration_type}")
|
||||
|
||||
return migrator.migrate(f"{migration_type.value.title()} Migration")
|
||||
@@ -1,12 +1,12 @@
|
||||
from functools import cached_property
|
||||
from sqlite3 import IntegrityError
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.dependencies import SharedDependencies
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema.group.group_shopping_list import (
|
||||
ShoppingListCreate,
|
||||
@@ -25,11 +25,13 @@ router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists
|
||||
|
||||
|
||||
@controller(router)
|
||||
class ShoppingListRoutes:
|
||||
deps: SharedDependencies = Depends(SharedDependencies.user)
|
||||
service: ShoppingListService = Depends(ShoppingListService.private)
|
||||
class ShoppingListController(BaseUserController):
|
||||
event_bus: EventBusService = Depends(EventBusService)
|
||||
|
||||
@cached_property
|
||||
def service(self):
|
||||
return ShoppingListService(self.repos)
|
||||
|
||||
@cached_property
|
||||
def repo(self):
|
||||
if not self.deps.acting_user:
|
||||
@@ -38,9 +40,9 @@ class ShoppingListRoutes:
|
||||
return self.deps.repos.group_shopping_lists.by_group(self.deps.acting_user.group_id)
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
Exception: "An unexpected error occurred.",
|
||||
IntegrityError: "An unexpected error occurred.",
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
44
mealie/routes/groups/controller_webhooks.py
Normal file
44
mealie/routes/groups/controller_webhooks.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema import mapper
|
||||
from mealie.schema.group.webhook import CreateWebhook, ReadWebhook, SaveWebhook
|
||||
from mealie.schema.query import GetAll
|
||||
|
||||
router = APIRouter(prefix="/groups/webhooks", tags=["Groups: Webhooks"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class ReadWebhookController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.webhooks.by_group(self.group_id)
|
||||
|
||||
@property
|
||||
def mixins(self) -> CrudMixins:
|
||||
return CrudMixins[CreateWebhook, SaveWebhook, CreateWebhook](self.repo, self.deps.logger)
|
||||
|
||||
@router.get("", response_model=list[ReadWebhook])
|
||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
||||
return self.repo.get_all(start=q.start, limit=q.limit, override_schema=ReadWebhook)
|
||||
|
||||
@router.post("", response_model=ReadWebhook, status_code=201)
|
||||
def create_one(self, data: CreateWebhook):
|
||||
save = mapper.cast(data, SaveWebhook, group_id=self.group.id)
|
||||
return self.mixins.create_one(save)
|
||||
|
||||
@router.get("/{item_id}", response_model=ReadWebhook)
|
||||
def get_one(self, item_id: int):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.put("/{item_id}", response_model=ReadWebhook)
|
||||
def update_one(self, item_id: int, data: CreateWebhook):
|
||||
return self.mixins.update_one(data, item_id)
|
||||
|
||||
@router.delete("/{item_id}", response_model=ReadWebhook)
|
||||
def delete_one(self, item_id: int):
|
||||
return self.mixins.delete_one(item_id) # type: ignore
|
||||
@@ -1,21 +0,0 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
|
||||
from mealie.schema.group.invite_token import CreateInviteToken, EmailInitationResponse, EmailInvitation, ReadInviteToken
|
||||
from mealie.services.group_services.group_service import GroupSelfService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=list[ReadInviteToken])
|
||||
def get_invite_tokens(g_service: GroupSelfService = Depends(GroupSelfService.private)):
|
||||
return g_service.get_invite_tokens()
|
||||
|
||||
|
||||
@router.post("", response_model=ReadInviteToken, status_code=status.HTTP_201_CREATED)
|
||||
def create_invite_token(uses: CreateInviteToken, g_service: GroupSelfService = Depends(GroupSelfService.private)):
|
||||
return g_service.create_invite_token(uses.uses)
|
||||
|
||||
|
||||
@router.post("/email", response_model=EmailInitationResponse)
|
||||
def email_invitation(invite: EmailInvitation, g_service: GroupSelfService = Depends(GroupSelfService.private)):
|
||||
return g_service.email_invitation(invite)
|
||||
@@ -1,27 +0,0 @@
|
||||
import shutil
|
||||
|
||||
from fastapi import Depends, File, Form
|
||||
from fastapi.datastructures import UploadFile
|
||||
|
||||
from mealie.core.dependencies import temporary_zip_path
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.group.group_migration import SupportedMigrations
|
||||
from mealie.schema.reports.reports import ReportSummary
|
||||
from mealie.services.group_services.migration_service import GroupMigrationService
|
||||
|
||||
router = UserAPIRouter()
|
||||
|
||||
|
||||
@router.post("", response_model=ReportSummary)
|
||||
def start_data_migration(
|
||||
add_migration_tag: bool = Form(False),
|
||||
migration_type: SupportedMigrations = Form(...),
|
||||
archive: UploadFile = File(...),
|
||||
temp_path: str = Depends(temporary_zip_path),
|
||||
gm_service: GroupMigrationService = Depends(GroupMigrationService.private),
|
||||
):
|
||||
# Save archive to temp_path
|
||||
with temp_path.open("wb") as buffer:
|
||||
shutil.copyfileobj(archive.file, buffer)
|
||||
|
||||
return gm_service.migrate(migration_type, add_migration_tag, temp_path)
|
||||
@@ -1,19 +0,0 @@
|
||||
from fastapi import Depends
|
||||
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.group.group_preferences import ReadGroupPreferences, UpdateGroupPreferences
|
||||
from mealie.services.group_services.group_service import GroupSelfService
|
||||
|
||||
router = UserAPIRouter()
|
||||
|
||||
|
||||
@router.put("", response_model=ReadGroupPreferences)
|
||||
def update_group_preferences(
|
||||
new_pref: UpdateGroupPreferences, g_service: GroupSelfService = Depends(GroupSelfService.write_existing)
|
||||
):
|
||||
return g_service.update_preferences(new_pref).preferences
|
||||
|
||||
|
||||
@router.get("", response_model=ReadGroupPreferences)
|
||||
def get_group_preferences(g_service: GroupSelfService = Depends(GroupSelfService.write_existing)):
|
||||
return g_service.item.preferences
|
||||
@@ -1,27 +0,0 @@
|
||||
from fastapi import Depends
|
||||
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.group.group_permissions import SetPermissions
|
||||
from mealie.schema.user.user import GroupInDB, UserOut
|
||||
from mealie.services.group_services.group_service import GroupSelfService
|
||||
|
||||
user_router = UserAPIRouter(prefix="/groups", tags=["Groups: Self Service"])
|
||||
|
||||
|
||||
@user_router.get("/self", response_model=GroupInDB)
|
||||
async def get_logged_in_user_group(g_service: GroupSelfService = Depends(GroupSelfService.write_existing)):
|
||||
"""Returns the Group Data for the Current User"""
|
||||
return g_service.item
|
||||
|
||||
|
||||
@user_router.get("/members", response_model=list[UserOut])
|
||||
async def get_group_members(g_service: GroupSelfService = Depends(GroupSelfService.write_existing)):
|
||||
"""Returns the Group of user lists"""
|
||||
return g_service.get_members()
|
||||
|
||||
|
||||
@user_router.put("/permissions", response_model=UserOut)
|
||||
async def set_member_permissions(
|
||||
payload: SetPermissions, g_service: GroupSelfService = Depends(GroupSelfService.manage_existing)
|
||||
):
|
||||
return g_service.set_member_permissions(payload)
|
||||
@@ -3,4 +3,4 @@ from fastapi import APIRouter
|
||||
from . import ingredient_parser
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(ingredient_parser.public_router, tags=["Recipe: Ingredient Parser"])
|
||||
router.include_router(ingredient_parser.router, tags=["Recipe: Ingredient Parser"])
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter
|
||||
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.schema.recipe import ParsedIngredient
|
||||
from mealie.schema.recipe.recipe_ingredient import IngredientRequest, IngredientsRequest
|
||||
from mealie.services.parser_services import IngredientParserService
|
||||
from mealie.services.parser_services import get_parser
|
||||
|
||||
public_router = APIRouter(prefix="/parser")
|
||||
router = APIRouter(prefix="/parser")
|
||||
|
||||
|
||||
@public_router.post("/ingredients", response_model=list[ParsedIngredient])
|
||||
def parse_ingredients(
|
||||
ingredients: IngredientsRequest,
|
||||
p_service: IngredientParserService = Depends(IngredientParserService.private),
|
||||
):
|
||||
p_service.set_parser(parser=ingredients.parser)
|
||||
return p_service.parse_ingredients(ingredients.ingredients)
|
||||
@controller(router)
|
||||
class IngredientParserController(BaseUserController):
|
||||
@router.post("/ingredients", response_model=list[ParsedIngredient])
|
||||
def parse_ingredients(self, ingredients: IngredientsRequest):
|
||||
parser = get_parser(ingredients.parser)
|
||||
return parser.parse(ingredients.ingredients)
|
||||
|
||||
|
||||
@public_router.post("/ingredient", response_model=ParsedIngredient)
|
||||
def parse_ingredient(
|
||||
ingredient: IngredientRequest,
|
||||
p_service: IngredientParserService = Depends(IngredientParserService.private),
|
||||
):
|
||||
p_service.set_parser(parser=ingredient.parser)
|
||||
return p_service.parse_ingredient(ingredient.ingredient)
|
||||
@router.post("/ingredient", response_model=ParsedIngredient)
|
||||
def parse_ingredient(self, ingredient: IngredientRequest):
|
||||
parser = get_parser(ingredient.parser)
|
||||
return parser.parse([ingredient.ingredient])[0]
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import (
|
||||
all_recipe_routes,
|
||||
bulk_actions,
|
||||
comments,
|
||||
image_and_assets,
|
||||
recipe_crud_routes,
|
||||
recipe_export,
|
||||
shared_routes,
|
||||
)
|
||||
from . import all_recipe_routes, bulk_actions, comments, image_and_assets, recipe_crud_routes, shared_routes
|
||||
|
||||
prefix = "/recipes"
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(all_recipe_routes.router, prefix=prefix, tags=["Recipe: Query All"])
|
||||
router.include_router(recipe_export.user_router, prefix=prefix, tags=["Recipe: Exports"])
|
||||
router.include_router(recipe_export.public_router, prefix=prefix, tags=["Recipe: Exports"])
|
||||
router.include_router(recipe_crud_routes.user_router, prefix=prefix, tags=["Recipe: CRUD"])
|
||||
router.include_router(image_and_assets.user_router, prefix=prefix, tags=["Recipe: Images and Assets"])
|
||||
router.include_router(recipe_crud_routes.router_exports)
|
||||
router.include_router(recipe_crud_routes.router)
|
||||
router.include_router(image_and_assets.router, prefix=prefix, tags=["Recipe: Images and Assets"])
|
||||
router.include_router(comments.router, prefix=prefix, tags=["Recipe: Comments"])
|
||||
router.include_router(bulk_actions.router, prefix=prefix, tags=["Recipe: Bulk Actions"])
|
||||
router.include_router(bulk_actions.export_router, prefix=prefix, tags=["Recipe: Bulk Exports"])
|
||||
router.include_router(bulk_actions.router, prefix=prefix)
|
||||
router.include_router(bulk_actions.router, prefix=prefix, tags=["Recipe: Bulk Exports"])
|
||||
router.include_router(shared_routes.router, prefix=prefix, tags=["Recipe: Shared"])
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from mealie.core.dependencies.dependencies import temporary_zip_path
|
||||
from mealie.core.security import create_file_token
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.schema.group.group_exports import GroupDataExport
|
||||
from mealie.schema.recipe.recipe_bulk_actions import (
|
||||
AssignCategories,
|
||||
@@ -12,66 +14,46 @@ from mealie.schema.recipe.recipe_bulk_actions import (
|
||||
DeleteRecipes,
|
||||
ExportRecipes,
|
||||
)
|
||||
from mealie.services.recipe.recipe_bulk_service import RecipeBulkActions
|
||||
from mealie.schema.response.responses import SuccessResponse
|
||||
from mealie.services.recipe.recipe_bulk_service import RecipeBulkActionsService
|
||||
|
||||
router = APIRouter(prefix="/bulk-actions")
|
||||
router = APIRouter(prefix="/bulk-actions", tags=["Recipe: Bulk Actions"])
|
||||
|
||||
|
||||
@router.post("/tag", response_model=BulkActionsResponse)
|
||||
def bulk_tag_recipes(
|
||||
tag_data: AssignTags,
|
||||
bulk_service: RecipeBulkActions = Depends(RecipeBulkActions.private),
|
||||
):
|
||||
bulk_service.assign_tags(tag_data.recipes, tag_data.tags)
|
||||
@controller(router)
|
||||
class RecipeBulkActionsController(BaseUserController):
|
||||
@cached_property
|
||||
def service(self) -> RecipeBulkActionsService:
|
||||
return RecipeBulkActionsService(self.repos, self.user, self.group)
|
||||
|
||||
@router.post("/tag", response_model=BulkActionsResponse)
|
||||
def bulk_tag_recipes(self, tag_data: AssignTags):
|
||||
self.service.assign_tags(tag_data.recipes, tag_data.tags)
|
||||
|
||||
@router.post("/categorize", response_model=BulkActionsResponse)
|
||||
def bulk_categorize_recipes(
|
||||
assign_cats: AssignCategories,
|
||||
bulk_service: RecipeBulkActions = Depends(RecipeBulkActions.private),
|
||||
):
|
||||
bulk_service.assign_categories(assign_cats.recipes, assign_cats.categories)
|
||||
@router.post("/categorize", response_model=BulkActionsResponse)
|
||||
def bulk_categorize_recipes(self, assign_cats: AssignCategories):
|
||||
self.service.assign_categories(assign_cats.recipes, assign_cats.categories)
|
||||
|
||||
@router.post("/delete", response_model=BulkActionsResponse)
|
||||
def bulk_delete_recipes(self, delete_recipes: DeleteRecipes):
|
||||
self.service.delete_recipes(delete_recipes.recipes)
|
||||
|
||||
@router.post("/delete", response_model=BulkActionsResponse)
|
||||
def bulk_delete_recipes(
|
||||
delete_recipes: DeleteRecipes,
|
||||
bulk_service: RecipeBulkActions = Depends(RecipeBulkActions.private),
|
||||
):
|
||||
bulk_service.delete_recipes(delete_recipes.recipes)
|
||||
@router.post("/export", status_code=202)
|
||||
def bulk_export_recipes(self, export_recipes: ExportRecipes, temp_path=Depends(temporary_zip_path)):
|
||||
self.service.export_recipes(temp_path, export_recipes.recipes)
|
||||
|
||||
@router.get("/export/download")
|
||||
def get_exported_data_token(self, path: Path):
|
||||
"""Returns a token to download a file"""
|
||||
|
||||
export_router = APIRouter(prefix="/bulk-actions")
|
||||
return {"fileToken": create_file_token(path)}
|
||||
|
||||
@router.get("/export", response_model=list[GroupDataExport])
|
||||
def get_exported_data(self):
|
||||
return self.service.get_exports()
|
||||
|
||||
@export_router.post("/export")
|
||||
def bulk_export_recipes(
|
||||
export_recipes: ExportRecipes,
|
||||
temp_path=Depends(temporary_zip_path),
|
||||
bulk_service: RecipeBulkActions = Depends(RecipeBulkActions.private),
|
||||
):
|
||||
bulk_service.export_recipes(temp_path, export_recipes.recipes)
|
||||
|
||||
# return FileResponse(temp_path, filename="recipes.zip")
|
||||
|
||||
|
||||
@export_router.get("/export/download")
|
||||
def get_exported_data_token(path: Path, _: RecipeBulkActions = Depends(RecipeBulkActions.private)):
|
||||
# return FileResponse(temp_path, filename="recipes.zip")
|
||||
"""Returns a token to download a file"""
|
||||
|
||||
return {"fileToken": create_file_token(path)}
|
||||
|
||||
|
||||
@export_router.get("/export", response_model=list[GroupDataExport])
|
||||
def get_exported_data(bulk_service: RecipeBulkActions = Depends(RecipeBulkActions.private)):
|
||||
return bulk_service.get_exports()
|
||||
|
||||
# return FileResponse(temp_path, filename="recipes.zip")
|
||||
|
||||
|
||||
@export_router.delete("/export/purge")
|
||||
def purge_export_data(bulk_service: RecipeBulkActions = Depends(RecipeBulkActions.private)):
|
||||
"""Remove all exports data, including items on disk without database entry"""
|
||||
amountDelete = bulk_service.purge_exports()
|
||||
return {"message": f"{amountDelete} exports deleted"}
|
||||
@router.delete("/export/purge", response_model=SuccessResponse)
|
||||
def purge_export_data(self):
|
||||
"""Remove all exports data, including items on disk without database entry"""
|
||||
amountDelete = self.service.purge_exports()
|
||||
return SuccessResponse.respond(f"{amountDelete} exports deleted")
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.schema.recipe.recipe_comments import RecipeCommentOut
|
||||
|
||||
router = UserAPIRouter()
|
||||
|
||||
|
||||
@router.get("/{slug}/comments", response_model=list[RecipeCommentOut])
|
||||
async def get_recipe_comments(
|
||||
slug: str,
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
"""Get all comments for a recipe"""
|
||||
db = get_repositories(session)
|
||||
recipe = db.recipes.get_one(slug)
|
||||
return db.comments.multi_query({"recipe_id": recipe.id})
|
||||
@controller(router)
|
||||
class RecipeCommentsController(BaseUserController):
|
||||
@router.get("/{slug}/comments", response_model=list[RecipeCommentOut])
|
||||
async def get_recipe_comments(self, slug: str):
|
||||
"""Get all comments for a recipe"""
|
||||
recipe = self.repos.recipes.by_group(self.group_id).get_one(slug)
|
||||
return self.repos.comments.multi_query({"recipe_id": recipe.id})
|
||||
|
||||
@@ -2,26 +2,30 @@ from shutil import copyfileobj
|
||||
|
||||
from fastapi import Depends, File, Form, HTTPException, status
|
||||
from fastapi.datastructures import UploadFile
|
||||
from pydantic import BaseModel
|
||||
from slugify import slugify
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.schema.recipe import CreateRecipeByUrl, Recipe, RecipeAsset
|
||||
from mealie.services.image.image import scrape_image, write_image
|
||||
|
||||
user_router = UserAPIRouter()
|
||||
router = UserAPIRouter()
|
||||
|
||||
|
||||
@user_router.post("/{slug}/image")
|
||||
class UpdateImageResponse(BaseModel):
|
||||
image: str
|
||||
|
||||
|
||||
@router.post("/{slug}/image")
|
||||
def scrape_image_url(slug: str, url: CreateRecipeByUrl):
|
||||
"""Removes an existing image and replaces it with the incoming file."""
|
||||
|
||||
scrape_image(url.url, slug)
|
||||
|
||||
|
||||
@user_router.put("/{slug}/image")
|
||||
@router.put("/{slug}/image", response_model=UpdateImageResponse)
|
||||
def update_recipe_image(
|
||||
slug: str,
|
||||
image: bytes = File(...),
|
||||
@@ -33,10 +37,10 @@ def update_recipe_image(
|
||||
write_image(slug, image, extension)
|
||||
new_version = db.recipes.update_image(slug, extension)
|
||||
|
||||
return {"image": new_version}
|
||||
return UpdateImageResponse(image=new_version)
|
||||
|
||||
|
||||
@user_router.post("/{slug}/assets", response_model=RecipeAsset)
|
||||
@router.post("/{slug}/assets", response_model=RecipeAsset)
|
||||
def upload_recipe_asset(
|
||||
slug: str,
|
||||
name: str = Form(...),
|
||||
|
||||
@@ -1,130 +1,245 @@
|
||||
from fastapi import Depends, File
|
||||
from functools import cached_property
|
||||
from zipfile import ZipFile
|
||||
|
||||
import sqlalchemy
|
||||
from fastapi import BackgroundTasks, Depends, File, HTTPException
|
||||
from fastapi.datastructures import UploadFile
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm.session import Session
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
from mealie.core import exceptions
|
||||
from mealie.core.dependencies import temporary_zip_path
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.core.dependencies.dependencies import temporary_dir, validate_recipe_token
|
||||
from mealie.core.security import create_recipe_slug_token
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.recipe import CreateRecipeByUrl, Recipe
|
||||
from mealie.repos.repository_recipes import RepositoryRecipes
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.schema.query import GetAll
|
||||
from mealie.schema.recipe import CreateRecipeByUrl, Recipe, RecipeImageTypes
|
||||
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary
|
||||
from mealie.schema.response.responses import ErrorResponse
|
||||
from mealie.schema.server.tasks import ServerTaskNames
|
||||
from mealie.services.recipe.recipe_service import RecipeService
|
||||
from mealie.services.recipe.template_service import TemplateService
|
||||
from mealie.services.scraper.scraper import create_from_url
|
||||
from mealie.services.scraper.scraper_strategies import RecipeScraperPackage
|
||||
from mealie.services.server_tasks.background_executory import BackgroundExecutor
|
||||
|
||||
user_router = UserAPIRouter()
|
||||
logger = get_logger()
|
||||
|
||||
class BaseRecipeController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self) -> RepositoryRecipes:
|
||||
return self.repos.recipes.by_group(self.group_id)
|
||||
|
||||
@cached_property
|
||||
def service(self) -> RecipeService:
|
||||
return RecipeService(self.repos, self.user, self.group)
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return CrudMixins[CreateRecipe, Recipe, Recipe](self.repo, self.deps.logger)
|
||||
|
||||
|
||||
@user_router.get("", response_model=list[RecipeSummary])
|
||||
async def get_all(
|
||||
start: int = 0,
|
||||
limit: int = None,
|
||||
load_foods: bool = False,
|
||||
service: RecipeService = Depends(RecipeService.private),
|
||||
):
|
||||
json_compatible_item_data = jsonable_encoder(service.get_all(start, limit, load_foods))
|
||||
return JSONResponse(content=json_compatible_item_data)
|
||||
class RecipeGetAll(GetAll):
|
||||
load_food: bool = False
|
||||
|
||||
|
||||
@user_router.post("", status_code=201, response_model=str)
|
||||
def create_from_name(data: CreateRecipe, recipe_service: RecipeService = Depends(RecipeService.private)) -> str:
|
||||
"""Takes in a JSON string and loads data into the database as a new entry"""
|
||||
return recipe_service.create_one(data).slug
|
||||
class FormatResponse(BaseModel):
|
||||
jjson: list[str] = Field(..., alias="json")
|
||||
zip: list[str]
|
||||
jinja2: list[str]
|
||||
|
||||
|
||||
@user_router.post("/create-url", status_code=201, response_model=str)
|
||||
def parse_recipe_url(url: CreateRecipeByUrl, recipe_service: RecipeService = Depends(RecipeService.private)):
|
||||
"""Takes in a URL and attempts to scrape data and load it into the database"""
|
||||
recipe = create_from_url(url.url)
|
||||
return recipe_service.create_one(recipe).slug
|
||||
router_exports = UserAPIRouter(prefix="/recipes", tags=["Recipe: Exports"])
|
||||
|
||||
|
||||
@user_router.post("/create-url/bulk", status_code=202)
|
||||
def parse_recipe_url_bulk(
|
||||
bulk: CreateRecipeByUrlBulk,
|
||||
recipe_service: RecipeService = Depends(RecipeService.private),
|
||||
bg_service: BackgroundExecutor = Depends(BackgroundExecutor.private),
|
||||
):
|
||||
"""Takes in a URL and attempts to scrape data and load it into the database"""
|
||||
@controller(router_exports)
|
||||
class RecipeExportController(BaseRecipeController):
|
||||
# ==================================================================================================================
|
||||
# Export Operations
|
||||
|
||||
def bulk_import_func(task_id: int, session: Session) -> None:
|
||||
database = get_repositories(session)
|
||||
task = database.server_tasks.get_one(task_id)
|
||||
@router_exports.get("/exports", response_model=FormatResponse)
|
||||
def get_recipe_formats_and_templates(self):
|
||||
return TemplateService().templates
|
||||
|
||||
task.append_log("test task has started")
|
||||
@router_exports.post("/{slug}/exports")
|
||||
def get_recipe_zip_token(self, slug: str):
|
||||
"""Generates a recipe zip token to be used to download a recipe as a zip file"""
|
||||
return {"token": create_recipe_slug_token(slug)}
|
||||
|
||||
for b in bulk.imports:
|
||||
try:
|
||||
recipe = create_from_url(b.url)
|
||||
@router_exports.get("/{slug}/exports", response_class=FileResponse)
|
||||
def get_recipe_as_format(self, slug: str, template_name: str, temp_dir=Depends(temporary_dir)):
|
||||
"""
|
||||
## Parameters
|
||||
`template_name`: The name of the template to use to use in the exports listed. Template type will automatically
|
||||
be set on the backend. Because of this, it's important that your templates have unique names. See available
|
||||
names and formats in the /api/recipes/exports endpoint.
|
||||
|
||||
if b.tags:
|
||||
recipe.tags = b.tags
|
||||
"""
|
||||
recipe = self.mixins.get_one(slug)
|
||||
file = self.service.render_template(recipe, temp_dir, template_name)
|
||||
return FileResponse(file)
|
||||
|
||||
if b.categories:
|
||||
recipe.recipe_category = b.categories
|
||||
@router_exports.get("/{slug}/exports/zip")
|
||||
def get_recipe_as_zip(self, slug: str, token: str, temp_path=Depends(temporary_zip_path)):
|
||||
"""Get a Recipe and It's Original Image as a Zip File"""
|
||||
slug = validate_recipe_token(token)
|
||||
|
||||
recipe_service.create_one(recipe)
|
||||
task.append_log(f"INFO: Created recipe from url: {b.url}")
|
||||
except Exception as e:
|
||||
task.append_log(f"Error: Failed to create recipe from url: {b.url}")
|
||||
task.append_log(f"Error: {e}")
|
||||
logger.error(f"Failed to create recipe from url: {b.url}")
|
||||
logger.error(e)
|
||||
if slug != slug:
|
||||
raise HTTPException(status_code=400, detail="Invalid Slug")
|
||||
|
||||
recipe: Recipe = self.mixins.get_one(slug)
|
||||
image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
|
||||
with ZipFile(temp_path, "w") as myzip:
|
||||
myzip.writestr(f"{slug}.json", recipe.json())
|
||||
|
||||
if image_asset.is_file():
|
||||
myzip.write(image_asset, arcname=image_asset.name)
|
||||
|
||||
return FileResponse(temp_path, filename=f"{slug}.zip")
|
||||
|
||||
|
||||
router = UserAPIRouter(prefix="/recipes", tags=["Recipe: CRUD"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class RecipeController(BaseRecipeController):
|
||||
# =======================================================================
|
||||
# URL Scraping Operations
|
||||
|
||||
@router.post("/create-url", status_code=201, response_model=str)
|
||||
def parse_recipe_url(self, url: CreateRecipeByUrl):
|
||||
"""Takes in a URL and attempts to scrape data and load it into the database"""
|
||||
recipe = create_from_url(url.url)
|
||||
return self.service.create_one(recipe).slug
|
||||
|
||||
@router.post("/create-url/bulk", status_code=202)
|
||||
def parse_recipe_url_bulk(self, bulk: CreateRecipeByUrlBulk, bg_tasks: BackgroundTasks):
|
||||
"""Takes in a URL and attempts to scrape data and load it into the database"""
|
||||
bg_executor = BackgroundExecutor(self.group.id, self.repos, bg_tasks)
|
||||
|
||||
def bulk_import_func(task_id: int, session: Session) -> None:
|
||||
database = get_repositories(session)
|
||||
task = database.server_tasks.get_one(task_id)
|
||||
|
||||
task.append_log("test task has started")
|
||||
|
||||
for b in bulk.imports:
|
||||
try:
|
||||
recipe = create_from_url(b.url)
|
||||
|
||||
if b.tags:
|
||||
recipe.tags = b.tags
|
||||
|
||||
if b.categories:
|
||||
recipe.recipe_category = b.categories
|
||||
|
||||
self.service.create_one(recipe)
|
||||
task.append_log(f"INFO: Created recipe from url: {b.url}")
|
||||
except Exception as e:
|
||||
task.append_log(f"Error: Failed to create recipe from url: {b.url}")
|
||||
task.append_log(f"Error: {e}")
|
||||
self.deps.logger.error(f"Failed to create recipe from url: {b.url}")
|
||||
self.deps.error(e)
|
||||
database.server_tasks.update(task.id, task)
|
||||
|
||||
task.set_finished()
|
||||
database.server_tasks.update(task.id, task)
|
||||
|
||||
task.set_finished()
|
||||
database.server_tasks.update(task.id, task)
|
||||
bg_executor.dispatch(ServerTaskNames.bulk_recipe_import, bulk_import_func)
|
||||
|
||||
bg_service.dispatch(ServerTaskNames.bulk_recipe_import, bulk_import_func)
|
||||
return {"details": "task has been started"}
|
||||
|
||||
return {"details": "task has been started"}
|
||||
@router.post("/test-scrape-url")
|
||||
def test_parse_recipe_url(self, url: CreateRecipeByUrl):
|
||||
# Debugger should produce the same result as the scraper sees before cleaning
|
||||
scraped_data = RecipeScraperPackage(url.url).scrape_url()
|
||||
|
||||
if scraped_data:
|
||||
return scraped_data.schema.data
|
||||
return "recipe_scrapers was unable to scrape this URL"
|
||||
|
||||
@user_router.post("/test-scrape-url")
|
||||
def test_parse_recipe_url(url: CreateRecipeByUrl):
|
||||
# Debugger should produce the same result as the scraper sees before cleaning
|
||||
scraped_data = RecipeScraperPackage(url.url).scrape_url()
|
||||
@router.post("/create-from-zip", status_code=201)
|
||||
def create_recipe_from_zip(self, temp_path=Depends(temporary_zip_path), archive: UploadFile = File(...)):
|
||||
"""Create recipe from archive"""
|
||||
recipe = self.service.create_from_zip(archive, temp_path)
|
||||
return recipe.slug
|
||||
|
||||
if scraped_data:
|
||||
return scraped_data.schema.data
|
||||
return "recipe_scrapers was unable to scrape this URL"
|
||||
# ==================================================================================================================
|
||||
# CRUD Operations
|
||||
|
||||
@router.get("", response_model=list[RecipeSummary])
|
||||
def get_all(self, q: RecipeGetAll = Depends(RecipeGetAll)):
|
||||
items = self.repo.summary(self.user.group_id, start=q.start, limit=q.limit, load_foods=q.load_food)
|
||||
|
||||
@user_router.post("/create-from-zip", status_code=201)
|
||||
async def create_recipe_from_zip(
|
||||
recipe_service: RecipeService = Depends(RecipeService.private),
|
||||
temp_path=Depends(temporary_zip_path),
|
||||
archive: UploadFile = File(...),
|
||||
):
|
||||
"""Create recipe from archive"""
|
||||
recipe = recipe_service.create_from_zip(archive, temp_path)
|
||||
return recipe.slug
|
||||
new_items = []
|
||||
for item in items:
|
||||
# Pydantic/FastAPI can't seem to serialize the ingredient field on thier own.
|
||||
new_item = item.__dict__
|
||||
|
||||
if q.load_food:
|
||||
new_item["recipe_ingredient"] = [x.__dict__ for x in item.recipe_ingredient]
|
||||
|
||||
@user_router.get("/{slug}", response_model=Recipe)
|
||||
def get_recipe(recipe_service: RecipeService = Depends(RecipeService.read_existing)):
|
||||
"""Takes in a recipe slug, returns all data for a recipe"""
|
||||
return recipe_service.item
|
||||
new_items.append(new_item)
|
||||
|
||||
json_compatible_item_data = jsonable_encoder(RecipeSummary.construct(**x) for x in new_items)
|
||||
|
||||
@user_router.put("/{slug}")
|
||||
def update_recipe(data: Recipe, recipe_service: RecipeService = Depends(RecipeService.write_existing)):
|
||||
"""Updates a recipe by existing slug and data."""
|
||||
return recipe_service.update_one(data)
|
||||
# Response is returned directly, to avoid validation and improve performance
|
||||
return JSONResponse(content=json_compatible_item_data)
|
||||
|
||||
@router.get("/{slug}", response_model=Recipe)
|
||||
def get_one(self, slug: str):
|
||||
"""Takes in a recipe slug, returns all data for a recipe"""
|
||||
return self.mixins.get_one(slug)
|
||||
|
||||
@user_router.patch("/{slug}")
|
||||
def patch_recipe(data: Recipe, recipe_service: RecipeService = Depends(RecipeService.write_existing)):
|
||||
"""Updates a recipe by existing slug and data."""
|
||||
return recipe_service.patch_one(data)
|
||||
@router.post("", status_code=201, response_model=str)
|
||||
def create_one(self, data: CreateRecipe) -> str:
|
||||
"""Takes in a JSON string and loads data into the database as a new entry"""
|
||||
try:
|
||||
return self.service.create_one(data).slug
|
||||
except Exception as e:
|
||||
self.handle_exceptions(e)
|
||||
|
||||
def handle_exceptions(self, ex: Exception) -> None:
|
||||
match type(ex):
|
||||
case exceptions.PermissionDenied:
|
||||
self.deps.logger.error("Permission Denied on recipe controller action")
|
||||
raise HTTPException(status_code=403, detail=ErrorResponse.respond(message="Permission Denied"))
|
||||
case exceptions.NoEntryFound:
|
||||
self.deps.logger.error("No Entry Found on recipe controller action")
|
||||
raise HTTPException(status_code=404, detail=ErrorResponse.respond(message="No Entry Found"))
|
||||
case sqlalchemy.exc.IntegrityError:
|
||||
self.deps.logger.error("SQL Integrity Error on recipe controller action")
|
||||
raise HTTPException(status_code=400, detail=ErrorResponse.respond(message="Recipe already exists"))
|
||||
|
||||
@user_router.delete("/{slug}")
|
||||
def delete_recipe(recipe_service: RecipeService = Depends(RecipeService.write_existing)):
|
||||
"""Deletes a recipe by slug"""
|
||||
return recipe_service.delete_one()
|
||||
@router.put("/{slug}")
|
||||
def update_one(self, slug: str, data: Recipe):
|
||||
"""Updates a recipe by existing slug and data."""
|
||||
try:
|
||||
data = self.service.update_one(slug, data)
|
||||
except Exception as e:
|
||||
self.handle_exceptions(e)
|
||||
|
||||
return data
|
||||
|
||||
@router.patch("/{slug}")
|
||||
def patch_one(self, slug: str, data: Recipe):
|
||||
"""Updates a recipe by existing slug and data."""
|
||||
try:
|
||||
data = self.service.patch_one(slug, data)
|
||||
except Exception as e:
|
||||
self.handle_exceptions(e)
|
||||
return data
|
||||
|
||||
@router.delete("/{slug}")
|
||||
def delete_one(self, slug: str):
|
||||
"""Deletes a recipe by slug"""
|
||||
try:
|
||||
return self.service.delete_one(slug)
|
||||
except Exception as e:
|
||||
self.handle_exceptions(e)
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
from zipfile import ZipFile
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm.session import Session
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
from mealie.core.dependencies import temporary_zip_path
|
||||
from mealie.core.dependencies.dependencies import temporary_dir, validate_recipe_token
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.core.security import create_recipe_slug_token
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.recipe import Recipe, RecipeImageTypes
|
||||
from mealie.services.recipe.recipe_service import RecipeService
|
||||
from mealie.services.recipe.template_service import TemplateService
|
||||
|
||||
user_router = UserAPIRouter()
|
||||
public_router = APIRouter()
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class FormatResponse(BaseModel):
|
||||
jjson: list[str] = Field(..., alias="json")
|
||||
zip: list[str]
|
||||
jinja2: list[str]
|
||||
|
||||
|
||||
@user_router.get("/exports", response_model=FormatResponse)
|
||||
async def get_recipe_formats_and_templates(_: RecipeService = Depends(RecipeService.private)):
|
||||
return TemplateService().templates
|
||||
|
||||
|
||||
@user_router.post("/{slug}/exports")
|
||||
async def get_recipe_zip_token(slug: str):
|
||||
"""Generates a recipe zip token to be used to download a recipe as a zip file"""
|
||||
return {"token": create_recipe_slug_token(slug)}
|
||||
|
||||
|
||||
@user_router.get("/{slug}/exports", response_class=FileResponse)
|
||||
def get_recipe_as_format(
|
||||
template_name: str,
|
||||
recipe_service: RecipeService = Depends(RecipeService.write_existing),
|
||||
temp_dir=Depends(temporary_dir),
|
||||
):
|
||||
"""
|
||||
## Parameters
|
||||
`template_name`: The name of the template to use to use in the exports listed. Template type will automatically
|
||||
be set on the backend. Because of this, it's important that your templates have unique names. See available
|
||||
names and formats in the /api/recipes/exports endpoint.
|
||||
|
||||
"""
|
||||
file = recipe_service.render_template(temp_dir, template_name)
|
||||
return FileResponse(file)
|
||||
|
||||
|
||||
@public_router.get("/{slug}/exports/zip")
|
||||
async def get_recipe_as_zip(
|
||||
token: str,
|
||||
slug: str,
|
||||
session: Session = Depends(generate_session),
|
||||
temp_path=Depends(temporary_zip_path),
|
||||
):
|
||||
"""Get a Recipe and It's Original Image as a Zip File"""
|
||||
slug = validate_recipe_token(token)
|
||||
|
||||
if slug != slug:
|
||||
raise HTTPException(status_code=400, detail="Invalid Slug")
|
||||
|
||||
db = get_repositories(session)
|
||||
recipe: Recipe = db.recipes.get(slug)
|
||||
image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
|
||||
with ZipFile(temp_path, "w") as myzip:
|
||||
myzip.writestr(f"{slug}.json", recipe.json())
|
||||
|
||||
if image_asset.is_file():
|
||||
myzip.write(image_asset, arcname=image_asset.name)
|
||||
|
||||
return FileResponse(temp_path, filename=f"{slug}.zip")
|
||||
@@ -1,23 +1,42 @@
|
||||
from fastapi import Depends
|
||||
from functools import cached_property
|
||||
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.services._base_http_service.router_factory import RouterFactory
|
||||
from mealie.services.shared.recipe_shared_service import RecipeShareTokenSummary, SharedRecipeService
|
||||
from pydantic import UUID4
|
||||
|
||||
router = UserAPIRouter(prefix="/shared")
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.schema.recipe import RecipeShareTokenSummary
|
||||
from mealie.schema.recipe.recipe_share_token import RecipeShareToken, RecipeShareTokenCreate, RecipeShareTokenSave
|
||||
|
||||
shared_router = RouterFactory(SharedRecipeService, prefix="/recipes", tags=["Shared: Recipes"])
|
||||
router = UserAPIRouter(prefix="/shared/recipes", tags=["Shared: Recipes"])
|
||||
|
||||
|
||||
@shared_router.get("", response_model=list[RecipeShareTokenSummary])
|
||||
def get_all_shared(
|
||||
recipe_id: int = None,
|
||||
shared_recipe_service: SharedRecipeService = Depends(SharedRecipeService.private),
|
||||
):
|
||||
"""
|
||||
Get all shared recipes
|
||||
"""
|
||||
return shared_recipe_service.get_all(recipe_id)
|
||||
@controller(router)
|
||||
class RecipeSharedController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.recipe_share_tokens.by_group(self.group_id)
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return CrudMixins[RecipeShareTokenSave, RecipeShareToken, RecipeShareTokenCreate](self.repo, self.deps.logger)
|
||||
|
||||
router.include_router(shared_router)
|
||||
@router.get("", response_model=list[RecipeShareTokenSummary])
|
||||
def get_all(self, recipe_id: int = None):
|
||||
if recipe_id:
|
||||
return self.repo.multi_query({"recipe_id": recipe_id}, override_schema=RecipeShareTokenSummary)
|
||||
else:
|
||||
return self.repo.get_all(override_schema=RecipeShareTokenSummary)
|
||||
|
||||
@router.post("", response_model=RecipeShareToken, status_code=201)
|
||||
def create_one(self, data: RecipeShareTokenCreate) -> RecipeShareToken:
|
||||
save_data = RecipeShareTokenSave(**data.dict(), group_id=self.group_id)
|
||||
return self.mixins.create_one(save_data)
|
||||
|
||||
@router.get("/{item_id}", response_model=RecipeShareToken)
|
||||
def get_one(self, item_id: UUID4):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.delete("/{item_id}")
|
||||
def delete_one(self, item_id: UUID4 = None) -> None:
|
||||
return self.mixins.delete_one(item_id)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import site_settings
|
||||
|
||||
settings_router = APIRouter()
|
||||
|
||||
settings_router.include_router(site_settings.public_router)
|
||||
settings_router.include_router(site_settings.admin_router)
|
||||
@@ -1,35 +0,0 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.dependencies import get_current_user
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
from mealie.schema.user import GroupInDB, PrivateUser
|
||||
from mealie.utils.post_webhooks import post_webhooks
|
||||
|
||||
public_router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
|
||||
admin_router = AdminAPIRouter(prefix="/api/site-settings", tags=["Settings"])
|
||||
|
||||
|
||||
@public_router.get("")
|
||||
def get_main_settings(session: Session = Depends(generate_session)):
|
||||
"""Returns basic site settings"""
|
||||
db = get_repositories(session)
|
||||
|
||||
return db.settings.get(1)
|
||||
|
||||
|
||||
@admin_router.post("/webhooks/test")
|
||||
def test_webhooks(
|
||||
current_user: PrivateUser = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
"""Run the function to test your webhooks"""
|
||||
db = get_repositories(session)
|
||||
group_entry: GroupInDB = db.groups.get(current_user.group, "name")
|
||||
|
||||
try:
|
||||
post_webhooks(group_entry.id, session)
|
||||
except Exception:
|
||||
return HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
@@ -1,11 +1,51 @@
|
||||
from fastapi import APIRouter
|
||||
from functools import cached_property
|
||||
|
||||
from . import tags
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
|
||||
prefix = "/tags"
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.schema.recipe import RecipeTagResponse, TagIn
|
||||
|
||||
router = APIRouter()
|
||||
router = APIRouter(prefix="/tags", tags=["Tags: CRUD"])
|
||||
|
||||
router.include_router(tags.public_router, prefix=prefix, tags=["Tags: CRUD"])
|
||||
router.include_router(tags.user_router, prefix=prefix, tags=["Tags: CRUD"])
|
||||
router.include_router(tags.admin_router, prefix=prefix, tags=["Tags: CRUD"])
|
||||
|
||||
@controller(router)
|
||||
class TagController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.tags
|
||||
|
||||
@router.get("")
|
||||
async def get_all_recipe_tags(self):
|
||||
"""Returns a list of available tags in the database"""
|
||||
return self.repo.get_all_limit_columns(["slug", "name"])
|
||||
|
||||
@router.get("/empty")
|
||||
def get_empty_tags(self):
|
||||
"""Returns a list of tags that do not contain any recipes"""
|
||||
return self.repo.get_empty()
|
||||
|
||||
@router.get("/{tag_slug}", response_model=RecipeTagResponse)
|
||||
def get_all_recipes_by_tag(self, tag_slug: str):
|
||||
"""Returns a list of recipes associated with the provided tag."""
|
||||
return self.repo.get_one(tag_slug, override_schema=RecipeTagResponse)
|
||||
|
||||
@router.post("", status_code=201)
|
||||
def create_recipe_tag(self, tag: TagIn):
|
||||
"""Creates a Tag in the database"""
|
||||
return self.repo.create(tag)
|
||||
|
||||
@router.put("/{tag_slug}", response_model=RecipeTagResponse)
|
||||
def update_recipe_tag(self, tag_slug: str, new_tag: TagIn):
|
||||
"""Updates an existing Tag in the database"""
|
||||
return self.repo.update(tag_slug, new_tag)
|
||||
|
||||
@router.delete("/{tag_slug}")
|
||||
def delete_recipe_tag(self, tag_slug: str):
|
||||
"""Removes a recipe tag from the database. Deleting a
|
||||
tag does not impact a recipe. The tag will be removed
|
||||
from any recipes that contain it"""
|
||||
|
||||
try:
|
||||
self.repo.delete(tag_slug)
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from mealie.core.dependencies import is_logged_in
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
|
||||
from mealie.schema.recipe import RecipeTagResponse, TagIn
|
||||
|
||||
public_router = APIRouter()
|
||||
user_router = UserAPIRouter()
|
||||
admin_router = AdminAPIRouter()
|
||||
|
||||
|
||||
@public_router.get("")
|
||||
async def get_all_recipe_tags(session: Session = Depends(generate_session)):
|
||||
"""Returns a list of available tags in the database"""
|
||||
db = get_repositories(session)
|
||||
return db.tags.get_all_limit_columns(["slug", "name"])
|
||||
|
||||
|
||||
@public_router.get("/empty")
|
||||
def get_empty_tags(session: Session = Depends(generate_session)):
|
||||
"""Returns a list of tags that do not contain any recipes"""
|
||||
db = get_repositories(session)
|
||||
return db.tags.get_empty()
|
||||
|
||||
|
||||
@public_router.get("/{tag}", response_model=RecipeTagResponse)
|
||||
def get_all_recipes_by_tag(
|
||||
tag: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
|
||||
):
|
||||
"""Returns a list of recipes associated with the provided tag."""
|
||||
db = get_repositories(session)
|
||||
tag_obj = db.tags.get(tag)
|
||||
tag_obj = RecipeTagResponse.from_orm(tag_obj)
|
||||
|
||||
if not is_user:
|
||||
tag_obj.recipes = [x for x in tag_obj.recipes if x.settings.public]
|
||||
|
||||
return tag_obj
|
||||
|
||||
|
||||
@user_router.post("")
|
||||
async def create_recipe_tag(tag: TagIn, session: Session = Depends(generate_session)):
|
||||
"""Creates a Tag in the database"""
|
||||
db = get_repositories(session)
|
||||
return db.tags.create(tag.dict())
|
||||
|
||||
|
||||
@admin_router.put("/{tag}", response_model=RecipeTagResponse)
|
||||
async def update_recipe_tag(tag: str, new_tag: TagIn, session: Session = Depends(generate_session)):
|
||||
"""Updates an existing Tag in the database"""
|
||||
db = get_repositories(session)
|
||||
return db.tags.update(tag, new_tag.dict())
|
||||
|
||||
|
||||
@admin_router.delete("/{tag}")
|
||||
async def delete_recipe_tag(tag: str, session: Session = Depends(generate_session)):
|
||||
"""Removes a recipe tag from the database. Deleting a
|
||||
tag does not impact a recipe. The tag will be removed
|
||||
from any recipes that contain it"""
|
||||
|
||||
try:
|
||||
db = get_repositories(session)
|
||||
db.tags.delete(tag)
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
@@ -1,18 +1,47 @@
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from mealie.schema.recipe.recipe_tool import RecipeToolResponse
|
||||
from mealie.services._base_http_service.router_factory import RouterFactory
|
||||
from mealie.services.recipe.recipe_tool_service import RecipeToolService
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema.query import GetAll
|
||||
from mealie.schema.recipe.recipe import RecipeTool
|
||||
from mealie.schema.recipe.recipe_tool import RecipeToolCreate, RecipeToolResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
tools_router = RouterFactory(RecipeToolService, prefix="/tools", tags=["Recipes: Tools"])
|
||||
router = APIRouter(prefix="/tools", tags=["Recipes: Tools"])
|
||||
|
||||
|
||||
@tools_router.get("/slug/{slug}")
|
||||
async def Func(slug: str, tools_service: RecipeToolService = Depends(RecipeToolService.private)):
|
||||
"""Returns a recipe by slug."""
|
||||
return tools_service.db.tools.get_one(slug, "slug", override_schema=RecipeToolResponse)
|
||||
@controller(router)
|
||||
class RecipeToolController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.tools
|
||||
|
||||
@property
|
||||
def mixins(self) -> CrudMixins:
|
||||
return CrudMixins[RecipeToolCreate, RecipeTool, RecipeToolCreate](self.repo, self.deps.logger)
|
||||
|
||||
router.include_router(tools_router)
|
||||
@router.get("", response_model=list[RecipeTool])
|
||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
||||
return self.repo.get_all(start=q.start, limit=q.limit, override_schema=RecipeTool)
|
||||
|
||||
@router.post("", response_model=RecipeTool, status_code=201)
|
||||
def create_one(self, data: RecipeToolCreate):
|
||||
return self.mixins.create_one(data)
|
||||
|
||||
@router.get("/{item_id}", response_model=RecipeTool)
|
||||
def get_one(self, item_id: int):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.put("/{item_id}", response_model=RecipeTool)
|
||||
def update_one(self, item_id: int, data: RecipeToolCreate):
|
||||
return self.mixins.update_one(data, item_id)
|
||||
|
||||
@router.delete("/{item_id}", response_model=RecipeTool)
|
||||
def delete_one(self, item_id: int):
|
||||
return self.mixins.delete_one(item_id) # type: ignore
|
||||
|
||||
@router.get("/slug/{tool_slug}", response_model=RecipeToolResponse)
|
||||
async def get_one_by_slug(self, tool_slug: str):
|
||||
return self.repo.get_one(tool_slug, "slug", override_schema=RecipeToolResponse)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from mealie.services._base_http_service.router_factory import RouterFactory
|
||||
from mealie.services.recipe.recipe_food_service import RecipeFoodService
|
||||
from mealie.services.recipe.recipe_unit_service import RecipeUnitService
|
||||
from . import foods, units
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(RouterFactory(RecipeFoodService, prefix="/foods", tags=["Recipes: Foods"]))
|
||||
router.include_router(RouterFactory(RecipeUnitService, prefix="/units", tags=["Recipes: Units"]))
|
||||
router.include_router(foods.router)
|
||||
router.include_router(units.router)
|
||||
|
||||
56
mealie/routes/unit_and_foods/foods.py
Normal file
56
mealie/routes/unit_and_foods/foods.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema.query import GetAll
|
||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit, IngredientUnit
|
||||
|
||||
router = APIRouter(prefix="/foods", tags=["Recipes: Foods"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class IngredientFoodsController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.deps.repos.ingredient_foods
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return CrudMixins[CreateIngredientUnit, IngredientUnit, CreateIngredientUnit](
|
||||
self.repo,
|
||||
self.deps.logger,
|
||||
self.registered_exceptions,
|
||||
)
|
||||
|
||||
@router.get("", response_model=list[IngredientUnit])
|
||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
||||
return self.repo.get_all(start=q.start, limit=q.limit)
|
||||
|
||||
@router.post("", response_model=IngredientUnit, status_code=201)
|
||||
def create_one(self, data: CreateIngredientUnit):
|
||||
return self.mixins.create_one(data)
|
||||
|
||||
@router.get("/{item_id}", response_model=IngredientUnit)
|
||||
def get_one(self, item_id: int):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.put("/{item_id}", response_model=IngredientUnit)
|
||||
def update_one(self, item_id: int, data: CreateIngredientUnit):
|
||||
return self.mixins.update_one(data, item_id)
|
||||
|
||||
@router.delete("/{item_id}", response_model=IngredientUnit)
|
||||
def delete_one(self, item_id: int):
|
||||
return self.mixins.delete_one(item_id)
|
||||
56
mealie/routes/unit_and_foods/units.py
Normal file
56
mealie/routes/unit_and_foods/units.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema.query import GetAll
|
||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit, IngredientUnit
|
||||
|
||||
router = APIRouter(prefix="/units", tags=["Recipes: Units"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class IngredientUnitsController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.deps.repos.ingredient_units
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return CrudMixins[CreateIngredientUnit, IngredientUnit, CreateIngredientUnit](
|
||||
self.repo,
|
||||
self.deps.logger,
|
||||
self.registered_exceptions,
|
||||
)
|
||||
|
||||
@router.get("", response_model=list[IngredientUnit])
|
||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
||||
return self.repo.get_all(start=q.start, limit=q.limit)
|
||||
|
||||
@router.post("", response_model=IngredientUnit, status_code=201)
|
||||
def create_one(self, data: CreateIngredientUnit):
|
||||
return self.mixins.create_one(data)
|
||||
|
||||
@router.get("/{item_id}", response_model=IngredientUnit)
|
||||
def get_one(self, item_id: int):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.put("/{item_id}", response_model=IngredientUnit)
|
||||
def update_one(self, item_id: int, data: CreateIngredientUnit):
|
||||
return self.mixins.update_one(data, item_id)
|
||||
|
||||
@router.delete("/{item_id}", response_model=IngredientUnit)
|
||||
def delete_one(self, item_id: int):
|
||||
return self.mixins.delete_one(item_id) # type: ignore
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import api_tokens, crud, favorites, images, passwords, registration
|
||||
from . import api_tokens, crud, favorites, forgot_password, images, registration
|
||||
|
||||
# Must be used because of the way FastAPI works with nested routes
|
||||
user_prefix = "/users"
|
||||
@@ -8,16 +8,9 @@ user_prefix = "/users"
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(registration.router, prefix=user_prefix, tags=["Users: Registration"])
|
||||
|
||||
router.include_router(crud.user_router, prefix=user_prefix, tags=["Users: CRUD"])
|
||||
router.include_router(crud.admin_router, prefix=user_prefix, tags=["Users: CRUD"])
|
||||
|
||||
router.include_router(passwords.user_router, prefix=user_prefix, tags=["Users: Passwords"])
|
||||
router.include_router(passwords.public_router, prefix=user_prefix, tags=["Users: Passwords"])
|
||||
|
||||
router.include_router(images.public_router, prefix=user_prefix, tags=["Users: Images"])
|
||||
router.include_router(images.user_router, prefix=user_prefix, tags=["Users: Images"])
|
||||
|
||||
router.include_router(api_tokens.router, prefix=user_prefix, tags=["Users: Tokens"])
|
||||
|
||||
router.include_router(favorites.user_router, prefix=user_prefix, tags=["Users: Favorites"])
|
||||
router.include_router(crud.user_router)
|
||||
router.include_router(crud.admin_router)
|
||||
router.include_router(forgot_password.router, prefix=user_prefix, tags=["Users: Passwords"])
|
||||
router.include_router(images.router, prefix=user_prefix, tags=["Users: Images"])
|
||||
router.include_router(api_tokens.router)
|
||||
router.include_router(favorites.router, prefix=user_prefix, tags=["Users: Favorites"])
|
||||
|
||||
@@ -1,61 +1,50 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from fastapi.param_functions import Depends
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.dependencies import get_current_user
|
||||
from mealie.core.security import create_access_token
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB, PrivateUser
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB
|
||||
|
||||
router = UserAPIRouter()
|
||||
router = UserAPIRouter(prefix="/users", tags=["Users: Tokens"])
|
||||
|
||||
|
||||
@router.post("/api-tokens", status_code=status.HTTP_201_CREATED)
|
||||
async def create_api_token(
|
||||
token_name: LoingLiveTokenIn,
|
||||
current_user: PrivateUser = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
"""Create api_token in the Database"""
|
||||
@controller(router)
|
||||
class UserApiTokensController(BaseUserController):
|
||||
@router.post("/api-tokens", status_code=status.HTTP_201_CREATED)
|
||||
def create_api_token(
|
||||
self,
|
||||
token_name: LoingLiveTokenIn,
|
||||
):
|
||||
"""Create api_token in the Database"""
|
||||
|
||||
token_data = {"long_token": True, "id": str(current_user.id)}
|
||||
token_data = {"long_token": True, "id": str(self.user.id)}
|
||||
|
||||
five_years = timedelta(1825)
|
||||
token = create_access_token(token_data, five_years)
|
||||
five_years = timedelta(1825)
|
||||
token = create_access_token(token_data, five_years)
|
||||
|
||||
token_model = CreateToken(
|
||||
name=token_name.name,
|
||||
token=token,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
token_model = CreateToken(
|
||||
name=token_name.name,
|
||||
token=token,
|
||||
user_id=self.user.id,
|
||||
)
|
||||
|
||||
db = get_repositories(session)
|
||||
new_token_in_db = self.repos.api_tokens.create(token_model)
|
||||
|
||||
new_token_in_db = db.api_tokens.create(token_model)
|
||||
if new_token_in_db:
|
||||
return {"token": token}
|
||||
|
||||
if new_token_in_db:
|
||||
return {"token": token}
|
||||
@router.delete("/api-tokens/{token_id}")
|
||||
def delete_api_token(self, token_id: int):
|
||||
"""Delete api_token from the Database"""
|
||||
token: LongLiveTokenInDB = self.repos.api_tokens.get(token_id)
|
||||
|
||||
if not token:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Could not locate token with id '{token_id}' in database")
|
||||
|
||||
@router.delete("/api-tokens/{token_id}")
|
||||
async def delete_api_token(
|
||||
token_id: int,
|
||||
current_user: PrivateUser = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
"""Delete api_token from the Database"""
|
||||
db = get_repositories(session)
|
||||
token: LongLiveTokenInDB = db.api_tokens.get(token_id)
|
||||
|
||||
if not token:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, f"Could not locate token with id '{token_id}' in database")
|
||||
|
||||
if token.user.email == current_user.email:
|
||||
deleted_token = db.api_tokens.delete(token_id)
|
||||
return {"token_delete": deleted_token.name}
|
||||
else:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
if token.user.email == self.user.email:
|
||||
deleted_token = self.repos.api_tokens.delete(token_id)
|
||||
return {"token_delete": deleted_token.name}
|
||||
else:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
@@ -1,100 +1,79 @@
|
||||
from fastapi import BackgroundTasks, Depends, HTTPException, status
|
||||
from fastapi import HTTPException, status
|
||||
from pydantic import UUID4
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core import security
|
||||
from mealie.core.dependencies import get_current_user
|
||||
from mealie.core.security import hash_password
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
|
||||
from mealie.core.security import hash_password, verify_password
|
||||
from mealie.routes._base import BaseAdminController, controller
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.routes._base.routers import AdminAPIRouter, UserAPIRouter
|
||||
from mealie.routes.users._helpers import assert_user_change_allowed
|
||||
from mealie.schema.user import PrivateUser, UserBase, UserIn, UserOut
|
||||
from mealie.services.events import create_user_event
|
||||
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserOut
|
||||
|
||||
user_router = UserAPIRouter(prefix="")
|
||||
admin_router = AdminAPIRouter(prefix="")
|
||||
user_router = UserAPIRouter(prefix="/users", tags=["Users: CRUD"])
|
||||
admin_router = AdminAPIRouter(prefix="/users", tags=["Users: Admin CRUD"])
|
||||
|
||||
|
||||
@admin_router.get("", response_model=list[UserOut])
|
||||
async def get_all_users(session: Session = Depends(generate_session)):
|
||||
db = get_repositories(session)
|
||||
return db.users.get_all()
|
||||
@controller(admin_router)
|
||||
class AdminUserController(BaseAdminController):
|
||||
@property
|
||||
def mixins(self) -> CrudMixins:
|
||||
return CrudMixins[UserIn, UserOut, UserBase](self.repos.users, self.deps.logger)
|
||||
|
||||
@admin_router.get("", response_model=list[UserOut])
|
||||
def get_all_users(self):
|
||||
return self.repos.users.get_all()
|
||||
|
||||
@admin_router.post("", response_model=UserOut, status_code=201)
|
||||
def create_user(self, new_user: UserIn):
|
||||
new_user.password = hash_password(new_user.password)
|
||||
return self.mixins.create_one(new_user)
|
||||
|
||||
@admin_router.get("/{item_id}", response_model=UserOut)
|
||||
def get_user(self, item_id: UUID4):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@admin_router.delete("/{item_id}")
|
||||
def delete_user(self, item_id: UUID4):
|
||||
"""Removes a user from the database. Must be the current user or a super user"""
|
||||
|
||||
assert_user_change_allowed(item_id, self.user)
|
||||
|
||||
if item_id == 1: # TODO: identify super_user
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER")
|
||||
|
||||
self.mixins.delete_one(item_id)
|
||||
|
||||
|
||||
@admin_router.post("", response_model=UserOut, status_code=201)
|
||||
async def create_user(
|
||||
background_tasks: BackgroundTasks,
|
||||
new_user: UserIn,
|
||||
current_user: PrivateUser = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
@controller(user_router)
|
||||
class UserController(BaseUserController):
|
||||
@user_router.get("/self", response_model=UserOut)
|
||||
def get_logged_in_user(self):
|
||||
return self.user
|
||||
|
||||
new_user.password = hash_password(new_user.password)
|
||||
background_tasks.add_task(
|
||||
create_user_event, "User Created", f"Created by {current_user.full_name}", session=session
|
||||
)
|
||||
@user_router.put("/{item_id}")
|
||||
def update_user(self, item_id: UUID4, new_data: UserBase):
|
||||
assert_user_change_allowed(item_id, self.user)
|
||||
|
||||
db = get_repositories(session)
|
||||
return db.users.create(new_user.dict())
|
||||
if not self.user.admin and (new_data.admin or self.user.group != new_data.group):
|
||||
# prevent a regular user from doing admin tasks on themself
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
if self.user.id == item_id and self.user.admin and not new_data.admin:
|
||||
# prevent an admin from demoting themself
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
@admin_router.get("/{id}", response_model=UserOut)
|
||||
async def get_user(id: UUID4, session: Session = Depends(generate_session)):
|
||||
db = get_repositories(session)
|
||||
return db.users.get(id)
|
||||
self.repos.users.update(item_id, new_data.dict())
|
||||
|
||||
if self.user.id == item_id:
|
||||
access_token = security.create_access_token(data=dict(sub=new_data.email))
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
@admin_router.delete("/{id}")
|
||||
def delete_user(
|
||||
id: UUID4,
|
||||
background_tasks: BackgroundTasks,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user: PrivateUser = Depends(get_current_user),
|
||||
):
|
||||
"""Removes a user from the database. Must be the current user or a super user"""
|
||||
@user_router.put("/{item_id}/password")
|
||||
def update_password(self, password_change: ChangePassword):
|
||||
"""Resets the User Password"""
|
||||
if not verify_password(password_change.current_password, self.user.password):
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
assert_user_change_allowed(id, current_user)
|
||||
|
||||
if id == 1: # TODO: identify super_user
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER")
|
||||
|
||||
try:
|
||||
db = get_repositories(session)
|
||||
db.users.delete(id)
|
||||
background_tasks.add_task(create_user_event, "User Deleted", f"User ID: {id}", session=session)
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@user_router.get("/self", response_model=UserOut)
|
||||
async def get_logged_in_user(
|
||||
current_user: PrivateUser = Depends(get_current_user),
|
||||
):
|
||||
return current_user.dict()
|
||||
|
||||
|
||||
@user_router.put("/{id}")
|
||||
async def update_user(
|
||||
id: UUID4,
|
||||
new_data: UserBase,
|
||||
current_user: PrivateUser = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
||||
assert_user_change_allowed(id, current_user)
|
||||
|
||||
if not current_user.admin and (new_data.admin or current_user.group != new_data.group):
|
||||
# prevent a regular user from doing admin tasks on themself
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
if current_user.id == id and current_user.admin and not new_data.admin:
|
||||
# prevent an admin from demoting themself
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
db = get_repositories(session)
|
||||
db.users.update(id, new_data.dict())
|
||||
|
||||
if current_user.id == id:
|
||||
access_token = security.create_access_token(data=dict(sub=new_data.email))
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
self.user.password = hash_password(password_change.new_password)
|
||||
return self.repos.users.update_password(self.user.id, self.user.password)
|
||||
|
||||
@@ -1,48 +1,31 @@
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm.session import Session
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.dependencies import get_current_user
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.routes.users._helpers import assert_user_change_allowed
|
||||
from mealie.schema.user import PrivateUser, UserFavorites
|
||||
from mealie.schema.user import UserFavorites
|
||||
|
||||
user_router = UserAPIRouter()
|
||||
router = UserAPIRouter()
|
||||
|
||||
|
||||
@user_router.get("/{id}/favorites", response_model=UserFavorites)
|
||||
async def get_favorites(id: str, session: Session = Depends(generate_session)):
|
||||
"""Get user's favorite recipes"""
|
||||
db = get_repositories(session)
|
||||
return db.users.get(id, override_schema=UserFavorites)
|
||||
@controller(router)
|
||||
class UserFavoritesController(BaseUserController):
|
||||
@router.get("/{id}/favorites", response_model=UserFavorites)
|
||||
async def get_favorites(self, id: UUID4):
|
||||
"""Get user's favorite recipes"""
|
||||
return self.repos.users.get(id, override_schema=UserFavorites)
|
||||
|
||||
@router.post("/{id}/favorites/{slug}")
|
||||
def add_favorite(self, id: UUID4, slug: str):
|
||||
"""Adds a Recipe to the users favorites"""
|
||||
assert_user_change_allowed(id, self.user)
|
||||
self.user.favorite_recipes.append(slug)
|
||||
self.repos.users.update(self.user.id, self.user)
|
||||
|
||||
@user_router.post("/{id}/favorites/{slug}")
|
||||
def add_favorite(
|
||||
slug: str,
|
||||
current_user: PrivateUser = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
"""Adds a Recipe to the users favorites"""
|
||||
|
||||
current_user.favorite_recipes.append(slug)
|
||||
db = get_repositories(session)
|
||||
db.users.update(current_user.id, current_user)
|
||||
|
||||
|
||||
@user_router.delete("/{id}/favorites/{slug}")
|
||||
def remove_favorite(
|
||||
slug: str,
|
||||
current_user: PrivateUser = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
"""Adds a Recipe to the users favorites"""
|
||||
|
||||
assert_user_change_allowed(id, current_user)
|
||||
current_user.favorite_recipes = [x for x in current_user.favorite_recipes if x != slug]
|
||||
|
||||
db = get_repositories(session)
|
||||
db.users.update(current_user.id, current_user)
|
||||
|
||||
return
|
||||
@router.delete("/{id}/favorites/{slug}")
|
||||
def remove_favorite(self, id: UUID4, slug: str):
|
||||
"""Adds a Recipe to the users favorites"""
|
||||
assert_user_change_allowed(id, self.user)
|
||||
self.user.favorite_recipes = [x for x in self.user.favorite_recipes if x != slug]
|
||||
self.repos.users.update(self.user.id, self.user)
|
||||
return
|
||||
|
||||
22
mealie/routes/users/forgot_password.py
Normal file
22
mealie/routes/users/forgot_password.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.schema.user.user_passwords import ForgotPassword, ResetPassword
|
||||
from mealie.services.user_services.password_reset_service import PasswordResetService
|
||||
|
||||
router = APIRouter(prefix="")
|
||||
|
||||
|
||||
@router.post("/forgot-password")
|
||||
def forgot_password(email: ForgotPassword, session: Session = Depends(generate_session)):
|
||||
"""Sends an email with a reset link to the user"""
|
||||
f_service = PasswordResetService(session)
|
||||
return f_service.send_reset_email(email.email)
|
||||
|
||||
|
||||
@router.post("/reset-password")
|
||||
def reset_password(reset_password: ResetPassword, session: Session = Depends(generate_session)):
|
||||
"""Resets the user password"""
|
||||
f_service = PasswordResetService(session)
|
||||
return f_service.reset_password(reset_password.token, reset_password.password)
|
||||
@@ -2,48 +2,41 @@ import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi.routing import APIRouter
|
||||
from pydantic import UUID4
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie import utils
|
||||
from mealie.core.dependencies import get_current_user
|
||||
from mealie.core.dependencies.dependencies import temporary_dir
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.routes.users._helpers import assert_user_change_allowed
|
||||
from mealie.schema.user import PrivateUser
|
||||
from mealie.services.image import minify
|
||||
|
||||
public_router = APIRouter(prefix="", tags=["Users: Images"])
|
||||
user_router = UserAPIRouter(prefix="", tags=["Users: Images"])
|
||||
router = UserAPIRouter(prefix="", tags=["Users: Images"])
|
||||
|
||||
|
||||
@user_router.post("/{id}/image")
|
||||
def update_user_image(
|
||||
id: UUID4,
|
||||
profile: UploadFile = File(...),
|
||||
temp_dir: Path = Depends(temporary_dir),
|
||||
current_user: PrivateUser = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
"""Updates a User Image"""
|
||||
assert_user_change_allowed(id, current_user)
|
||||
@controller(router)
|
||||
class UserImageController(BaseUserController):
|
||||
@router.post("/{id}/image")
|
||||
def update_user_image(
|
||||
self,
|
||||
id: UUID4,
|
||||
profile: UploadFile = File(...),
|
||||
temp_dir: Path = Depends(temporary_dir),
|
||||
):
|
||||
"""Updates a User Image"""
|
||||
assert_user_change_allowed(id, self.user)
|
||||
temp_img = temp_dir.joinpath(profile.filename)
|
||||
|
||||
temp_img = temp_dir.joinpath(profile.filename)
|
||||
with temp_img.open("wb") as buffer:
|
||||
shutil.copyfileobj(profile.file, buffer)
|
||||
|
||||
with temp_img.open("wb") as buffer:
|
||||
shutil.copyfileobj(profile.file, buffer)
|
||||
image = minify.to_webp(temp_img)
|
||||
dest = PrivateUser.get_directory(id) / "profile.webp"
|
||||
|
||||
image = minify.to_webp(temp_img)
|
||||
dest = PrivateUser.get_directory(id) / "profile.webp"
|
||||
shutil.copyfile(image, dest)
|
||||
|
||||
shutil.copyfile(image, dest)
|
||||
self.repos.users.patch(id, {"cache_key": utils.new_cache_key()})
|
||||
|
||||
db = get_repositories(session)
|
||||
|
||||
db.users.patch(id, {"cache_key": utils.new_cache_key()})
|
||||
|
||||
if not dest.is_file:
|
||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
if not dest.is_file:
|
||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.config import get_app_settings
|
||||
from mealie.core.security import hash_password
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.user import ChangePassword
|
||||
from mealie.schema.user.user_passwords import ForgotPassword, ResetPassword
|
||||
from mealie.services.user_services import UserService
|
||||
from mealie.services.user_services.password_reset_service import PasswordResetService
|
||||
|
||||
user_router = UserAPIRouter(prefix="")
|
||||
public_router = APIRouter(prefix="")
|
||||
settings = get_app_settings()
|
||||
|
||||
|
||||
@user_router.put("/{id}/reset-password")
|
||||
async def reset_user_password(id: int, session: Session = Depends(generate_session)):
|
||||
new_password = hash_password(settings.DEFAULT_PASSWORD)
|
||||
|
||||
db = get_repositories(session)
|
||||
db.users.update_password(id, new_password)
|
||||
|
||||
|
||||
@user_router.put("/{item_id}/password")
|
||||
def update_password(password_change: ChangePassword, user_service: UserService = Depends(UserService.write_existing)):
|
||||
"""Resets the User Password"""
|
||||
return user_service.change_password(password_change)
|
||||
|
||||
|
||||
@public_router.post("/forgot-password")
|
||||
def forgot_password(email: ForgotPassword, session: Session = Depends(generate_session)):
|
||||
"""Sends an email with a reset link to the user"""
|
||||
f_service = PasswordResetService(session)
|
||||
return f_service.send_reset_email(email.email)
|
||||
|
||||
|
||||
@public_router.post("/reset-password")
|
||||
def reset_password(reset_password: ResetPassword, session: Session = Depends(generate_session)):
|
||||
"""Resets the user password"""
|
||||
f_service = PasswordResetService(session)
|
||||
return f_service.reset_password(reset_password.token, reset_password.password)
|
||||
@@ -1,5 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi import APIRouter, status
|
||||
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes._base import BasePublicController, controller
|
||||
from mealie.schema.user.registration import CreateUserRegistration
|
||||
from mealie.schema.user.user import UserOut
|
||||
from mealie.services.user_services.registration_service import RegistrationService
|
||||
@@ -7,8 +9,9 @@ from mealie.services.user_services.registration_service import RegistrationServi
|
||||
router = APIRouter(prefix="/register")
|
||||
|
||||
|
||||
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
|
||||
def register_new_user(
|
||||
data: CreateUserRegistration, registration_service: RegistrationService = Depends(RegistrationService.public)
|
||||
):
|
||||
return registration_service.register_user(data)
|
||||
@controller(router)
|
||||
class RegistrationController(BasePublicController):
|
||||
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
|
||||
def register_new_user(self, data: CreateUserRegistration):
|
||||
registration_service = RegistrationService(self.deps.logger, get_repositories(self.deps.session))
|
||||
return registration_service.register_user(data)
|
||||
|
||||
Reference in New Issue
Block a user