mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -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:
parent
5823a32daf
commit
c4540f1395
1
.gitignore
vendored
1
.gitignore
vendored
@ -157,3 +157,4 @@ mealie/services/scraper/ingredient_nlp/model.crfmodel
|
||||
dev/code-generation/generated/openapi.json
|
||||
dev/code-generation/generated/test_routes.py
|
||||
mealie/services/parser_services/crfpp/model.crfmodel
|
||||
lcov.info
|
||||
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -37,7 +37,7 @@
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.flake8Enabled": true,
|
||||
"python.linting.pylintEnabled": false,
|
||||
"python.pythonPath": ".venv/bin/python3.9",
|
||||
"python.linting.pylintArgs": ["--rcfile=${workspaceFolder}/.pylintrc"],
|
||||
"python.testing.autoTestDiscoverOnSaveEnabled": false,
|
||||
"python.testing.pytestArgs": ["tests"],
|
||||
"python.testing.pytestEnabled": true,
|
||||
@ -45,5 +45,6 @@
|
||||
"python.analysis.typeCheckingMode": "off",
|
||||
"search.mode": "reuseEditor",
|
||||
"vetur.validation.template": false,
|
||||
"python.sortImports.path": "${workspaceFolder}/.venv/bin/isort"
|
||||
"python.sortImports.path": "${workspaceFolder}/.venv/bin/isort",
|
||||
"coverage-gutters.lcovname": "${workspaceFolder}/.coverage"
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
@ -51,8 +51,8 @@
|
||||
event: 'save',
|
||||
},
|
||||
]"
|
||||
@delete="actions.deleteOne(webhook.id)"
|
||||
@save="actions.updateOne(webhook)"
|
||||
@delete="actions.deleteOne(cookbook.id)"
|
||||
@save="actions.updateOne(cookbook)"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</v-expansion-panel-content>
|
||||
|
5
makefile
5
makefile
@ -59,11 +59,10 @@ lint: ## 🧺 Format, Check and Flake8
|
||||
poetry run black .
|
||||
poetry run flake8 mealie tests
|
||||
|
||||
|
||||
|
||||
coverage: ## ☂️ Check code coverage quickly with the default Python
|
||||
poetry run pytest
|
||||
poetry run pytest
|
||||
poetry run coverage report -m
|
||||
poetry run coveragepy-lcov
|
||||
poetry run coverage html
|
||||
$(BROWSER) htmlcov/index.html
|
||||
|
||||
|
@ -9,7 +9,6 @@ from mealie.routes import backup_routes, router, utility_routes
|
||||
from mealie.routes.about import about_router
|
||||
from mealie.routes.handlers import register_debug_handler
|
||||
from mealie.routes.media import media_router
|
||||
from mealie.routes.site_settings import settings_router
|
||||
from mealie.services.events import create_general_event
|
||||
from mealie.services.scheduler import SchedulerRegistry, SchedulerService, tasks
|
||||
|
||||
@ -50,7 +49,6 @@ def api_routers():
|
||||
app.include_router(router)
|
||||
app.include_router(media_router)
|
||||
app.include_router(about_router)
|
||||
app.include_router(settings_router)
|
||||
app.include_router(backup_routes.router)
|
||||
app.include_router(utility_routes.router)
|
||||
|
||||
|
@ -1,2 +1 @@
|
||||
from .dependencies import *
|
||||
from .grouped import *
|
||||
|
@ -1,74 +0,0 @@
|
||||
from fastapi import BackgroundTasks, Depends
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
|
||||
from .dependencies import generate_session, get_admin_user, get_current_user, is_logged_in
|
||||
|
||||
|
||||
class RequestContext:
|
||||
def __init__(
|
||||
self,
|
||||
background_tasks: BackgroundTasks,
|
||||
session: Session = Depends(generate_session),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
self.session: Session = session
|
||||
self.bg_task: BackgroundTasks = background_tasks
|
||||
self.user: bool = user
|
||||
|
||||
|
||||
class PublicDeps:
|
||||
"""
|
||||
PublicDeps contains the common dependencies for all read operations through the API.
|
||||
Note: The user object is used to definer what assets the user has access to.
|
||||
|
||||
Args:
|
||||
background_tasks: BackgroundTasks
|
||||
session: Session
|
||||
user: bool
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
background_tasks: BackgroundTasks,
|
||||
session: Session = Depends(generate_session),
|
||||
user=Depends(is_logged_in),
|
||||
):
|
||||
self.session: Session = session
|
||||
self.bg_task: BackgroundTasks = background_tasks
|
||||
self.user: bool = user
|
||||
|
||||
|
||||
class UserDeps:
|
||||
"""
|
||||
UserDeps contains the common dependencies for all read operations through the API.
|
||||
Note: The user must be logged in or the route will return a 401 error.
|
||||
|
||||
Args:
|
||||
background_tasks: BackgroundTasks
|
||||
session: Session
|
||||
user: UserInDB
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
background_tasks: BackgroundTasks,
|
||||
session: Session = Depends(generate_session),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
self.session: Session = session
|
||||
self.bg_task: BackgroundTasks = background_tasks
|
||||
self.user: PrivateUser = user
|
||||
|
||||
|
||||
class AdminDeps:
|
||||
def __init__(
|
||||
self,
|
||||
background_tasks: BackgroundTasks,
|
||||
session: Session = Depends(generate_session),
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
self.session: Session = session
|
||||
self.bg_task: BackgroundTasks = background_tasks
|
||||
self.user: PrivateUser = user
|
31
mealie/core/exceptions.py
Normal file
31
mealie/core/exceptions.py
Normal file
@ -0,0 +1,31 @@
|
||||
from sqlite3 import IntegrityError
|
||||
|
||||
from mealie.lang.providers import AbstractLocaleProvider
|
||||
|
||||
|
||||
class PermissionDenied(Exception):
|
||||
"""
|
||||
This exception is raised when a user tries to access a resource that they do not have permission to access.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NoEntryFound(Exception):
|
||||
"""
|
||||
This exception is raised when a user tries to access a resource that does not exist.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def mealie_registered_exceptions(t: AbstractLocaleProvider) -> dict:
|
||||
"""
|
||||
This function returns a dictionary of all the globally registered exceptions in the Mealie application.
|
||||
"""
|
||||
|
||||
return {
|
||||
PermissionDenied: t.t("exceptions.permission-denied"),
|
||||
NoEntryFound: t.t("exceptions.no-entry-found"),
|
||||
IntegrityError: t.t("exceptions.integrity-error"),
|
||||
}
|
@ -14,7 +14,6 @@ settings = get_app_settings()
|
||||
LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
|
||||
DATE_FORMAT = "%d-%b-%y %H:%M:%S"
|
||||
LOGGER_FORMAT = "%(levelname)s: %(asctime)s \t%(message)s"
|
||||
LOGGER_HANDLER = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
@ -8,10 +8,12 @@ from ..recipe.category import Category, cookbooks_to_categories
|
||||
class CookBook(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "cookbooks"
|
||||
id = Column(Integer, primary_key=True)
|
||||
position = Column(Integer, nullable=False)
|
||||
position = Column(Integer, nullable=False, default=1)
|
||||
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(String, default="")
|
||||
slug = Column(String, nullable=False)
|
||||
description = Column(String, default="")
|
||||
|
||||
categories = orm.relationship(Category, secondary=cookbooks_to_categories, single_parent=True)
|
||||
|
||||
group_id = Column(guid.GUID, ForeignKey("groups.id"))
|
||||
|
@ -88,7 +88,9 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
collection_class=ordering_list("position"),
|
||||
)
|
||||
|
||||
share_tokens = orm.relationship(RecipeShareTokenModel, back_populates="recipe")
|
||||
share_tokens = orm.relationship(
|
||||
RecipeShareTokenModel, back_populates="recipe", cascade="all, delete, delete-orphan"
|
||||
)
|
||||
|
||||
comments: list = orm.relationship("RecipeComment", back_populates="recipe", cascade="all, delete, delete-orphan")
|
||||
|
||||
|
@ -4,5 +4,10 @@
|
||||
},
|
||||
"recipe": {
|
||||
"unique-name-error": "Recipe names must be unique"
|
||||
},
|
||||
"exceptions": {
|
||||
"permission_denied": "You do not have permission to perform this action",
|
||||
"no-entry-found": "The requested resource was not found",
|
||||
"integrity-error": "Database integrity error"
|
||||
}
|
||||
}
|
@ -100,6 +100,7 @@ class AllRepositories:
|
||||
|
||||
@cached_property
|
||||
def categories(self) -> RepositoryCategories:
|
||||
# TODO: Fix Typing for Category Repository
|
||||
return RepositoryCategories(self.session, pk_slug, Category, RecipeCategoryResponse)
|
||||
|
||||
@cached_property
|
||||
|
@ -1,18 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Generic, TypeVar, Union
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import UUID4
|
||||
from pydantic import UUID4, BaseModel
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import load_only
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.root_logger import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
T = TypeVar("T")
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
D = TypeVar("D")
|
||||
|
||||
|
||||
@ -40,12 +34,12 @@ class RepositoryGeneric(Generic[T, D]):
|
||||
def subscribe(self, func: Callable) -> None:
|
||||
self.observers.append(func)
|
||||
|
||||
def by_user(self, user_id: UUID4) -> RepositoryGeneric:
|
||||
def by_user(self, user_id: UUID4) -> "RepositoryGeneric[T, D]":
|
||||
self.limit_by_user = True
|
||||
self.user_id = user_id
|
||||
return self
|
||||
|
||||
def by_group(self, group_id: UUID) -> RepositoryGeneric:
|
||||
def by_group(self, group_id: UUID) -> "RepositoryGeneric[T, D]":
|
||||
self.limit_by_group = True
|
||||
self.group_id = group_id
|
||||
return self
|
||||
@ -147,7 +141,7 @@ class RepositoryGeneric(Generic[T, D]):
|
||||
results_as_dict = [x.dict() for x in results]
|
||||
return [x.get(self.primary_key) for x in results_as_dict]
|
||||
|
||||
def _query_one(self, match_value: str, match_key: str = None) -> D:
|
||||
def _query_one(self, match_value: str | int | UUID4, match_key: str = None) -> D:
|
||||
"""
|
||||
Query the sql database for one item an return the sql alchemy model
|
||||
object. If no match key is provided the primary_key attribute will be used.
|
||||
@ -178,7 +172,7 @@ class RepositoryGeneric(Generic[T, D]):
|
||||
return eff_schema.from_orm(result)
|
||||
|
||||
def get(
|
||||
self, match_value: str, match_key: str = None, limit=1, any_case=False, override_schema=None
|
||||
self, match_value: str | int | UUID4, match_key: str = None, limit=1, any_case=False, override_schema=None
|
||||
) -> T | list[T]:
|
||||
"""Retrieves an entry from the database by matching a key/value pair. If no
|
||||
key is provided the class objects primary key will be used to match against.
|
||||
@ -237,7 +231,7 @@ class RepositoryGeneric(Generic[T, D]):
|
||||
|
||||
return self.schema.from_orm(new_document)
|
||||
|
||||
def update(self, match_value: str, new_data: dict) -> T:
|
||||
def update(self, match_value: str | int | UUID4, new_data: dict) -> T:
|
||||
"""Update a database entry.
|
||||
Args:
|
||||
session (Session): Database Session
|
||||
@ -258,7 +252,7 @@ class RepositoryGeneric(Generic[T, D]):
|
||||
self.session.commit()
|
||||
return self.schema.from_orm(entry)
|
||||
|
||||
def patch(self, match_value: str, new_data: dict) -> T:
|
||||
def patch(self, match_value: str | int | UUID4, new_data: dict) -> T:
|
||||
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
|
||||
|
||||
entry = self._query_one(match_value=match_value)
|
||||
|
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)
|
||||
|
@ -30,3 +30,4 @@ class CheckAppConfig(CamelModel):
|
||||
email_ready: bool = False
|
||||
ldap_ready: bool = False
|
||||
base_url_set: bool = False
|
||||
is_up_to_date: bool = False
|
||||
|
@ -12,50 +12,18 @@ class BackupOptions(BaseModel):
|
||||
users: bool = True
|
||||
notifications: bool = True
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"recipes": True,
|
||||
"settings": True,
|
||||
"themes": True,
|
||||
"groups": True,
|
||||
"users": True,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ImportJob(BackupOptions):
|
||||
name: str
|
||||
force: bool = False
|
||||
rebase: bool = False
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "my_local_backup.zip",
|
||||
"recipes": True,
|
||||
"settings": True,
|
||||
"themes": True,
|
||||
"groups": True,
|
||||
"users": True,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CreateBackup(BaseModel):
|
||||
tag: Optional[str]
|
||||
options: BackupOptions
|
||||
templates: Optional[List[str]]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"tag": "July 23rd 2021",
|
||||
"options": BackupOptions(),
|
||||
"template": ["recipes.md"],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BackupFile(BaseModel):
|
||||
name: str
|
||||
@ -66,16 +34,3 @@ class BackupFile(BaseModel):
|
||||
class AllBackups(BaseModel):
|
||||
imports: List[BackupFile]
|
||||
templates: List[str]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"imports": [
|
||||
{
|
||||
"name": "AutoBackup_12-1-2020.zip",
|
||||
"date": datetime.now(),
|
||||
}
|
||||
],
|
||||
"templates": ["recipes.md", "custom_template.md"],
|
||||
}
|
||||
}
|
||||
|
@ -9,13 +9,6 @@ from .restore import RecipeImport
|
||||
class ChowdownURL(BaseModel):
|
||||
url: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"url": "https://chowdownrepo.com/repo",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MigrationFile(BaseModel):
|
||||
name: str
|
||||
|
@ -1,7 +1,5 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
from pydantic import validator
|
||||
from pydantic import UUID4, validator
|
||||
from slugify import slugify
|
||||
|
||||
from ..recipe.recipe_category import CategoryBase, RecipeCategoryResponse
|
||||
@ -25,16 +23,16 @@ class CreateCookBook(CamelModel):
|
||||
return slug
|
||||
|
||||
|
||||
class UpdateCookBook(CreateCookBook):
|
||||
class SaveCookBook(CreateCookBook):
|
||||
group_id: UUID4
|
||||
|
||||
|
||||
class UpdateCookBook(SaveCookBook):
|
||||
id: int
|
||||
|
||||
|
||||
class SaveCookBook(CreateCookBook):
|
||||
group_id: UUID
|
||||
|
||||
|
||||
class ReadCookBook(UpdateCookBook):
|
||||
group_id: UUID
|
||||
group_id: UUID4
|
||||
categories: list[CategoryBase] = []
|
||||
|
||||
class Config:
|
||||
@ -42,7 +40,7 @@ class ReadCookBook(UpdateCookBook):
|
||||
|
||||
|
||||
class RecipeCookBook(ReadCookBook):
|
||||
group_id: UUID
|
||||
group_id: UUID4
|
||||
categories: list[RecipeCategoryResponse]
|
||||
|
||||
class Config:
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
@ -1,2 +1,2 @@
|
||||
# GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
from .error_response import *
|
||||
from .responses import *
|
||||
|
@ -15,3 +15,16 @@ class ErrorResponse(BaseModel):
|
||||
in the same call, for use while providing details to a HTTPException
|
||||
"""
|
||||
return cls(message=message, exception=exception).dict()
|
||||
|
||||
|
||||
class SuccessResponse(BaseModel):
|
||||
message: str
|
||||
error: bool = False
|
||||
|
||||
@classmethod
|
||||
def respond(cls, message: str) -> dict:
|
||||
"""
|
||||
This method is an helper to create an obect and convert to a dictionary
|
||||
in the same call, for use while providing details to a HTTPException
|
||||
"""
|
||||
return cls(message=message).dict()
|
1
mealie/schema/static/__init__.py
Normal file
1
mealie/schema/static/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .recipe_keys import *
|
136
mealie/schema/static/recipe_keys.py
Normal file
136
mealie/schema/static/recipe_keys.py
Normal file
@ -0,0 +1,136 @@
|
||||
cook_time = "cookTime"
|
||||
cooking_method = "cookingMethod"
|
||||
nutrition = "nutrition"
|
||||
recipe_category = "recipeCategory"
|
||||
recipe_cuisine = "recipeCuisine"
|
||||
recipe_ingredient = "recipeIngredient"
|
||||
recipe_instructions = "recipeInstructions"
|
||||
recipe_yield = "recipeYield"
|
||||
suitable_for_diet = "suitableForDiet"
|
||||
estimated_cost = "estimatedCost"
|
||||
perform_time = "performTime"
|
||||
prep_time = "prepTime"
|
||||
step = "step"
|
||||
supply = "supply"
|
||||
tool = "tool"
|
||||
total_time = "totalTime"
|
||||
y_yield = "yield"
|
||||
about = "about"
|
||||
abstract = "abstract"
|
||||
access_mode = "accessMode"
|
||||
access_mode_sufficient = "accessModeSufficient"
|
||||
accessibility_api = "accessibilityAPI"
|
||||
accessibility_control = "accessibilityControl"
|
||||
accessibility_feature = "accessibilityFeature"
|
||||
accessibility_hazard = "accessibilityHazard"
|
||||
accessibility_summary = "accessibilitySummary"
|
||||
accountable_person = "accountablePerson"
|
||||
acquire_license_page = "acquireLicensePage"
|
||||
aggregate_rating = "aggregateRating"
|
||||
alternative_headline = "alternativeHeadline"
|
||||
archived_at = "archivedAt"
|
||||
assesses = "assesses"
|
||||
associated_media = "associatedMedia"
|
||||
audience = "audience"
|
||||
audio = "audio"
|
||||
author = "author"
|
||||
award = "award"
|
||||
character = "character"
|
||||
citation = "citation"
|
||||
comment = "comment"
|
||||
comment_count = "commentCount"
|
||||
conditions_of_access = "conditionsOfAccess"
|
||||
content_location = "contentLocation"
|
||||
content_rating = "contentRating"
|
||||
content_reference_time = "contentReferenceTime"
|
||||
contributor = "contributor"
|
||||
copyright_holder = "copyrightHolder"
|
||||
copyright_notice = "copyrightNotice"
|
||||
copyright_year = "copyrightYear"
|
||||
correction = "correction"
|
||||
country_of_origin = "countryOfOrigin"
|
||||
creative_work_status = "creativeWorkStatus"
|
||||
creator = "creator"
|
||||
credit_text = "creditText"
|
||||
date_created = "dateCreated"
|
||||
date_created = "dateCreated"
|
||||
date_modified = "dateModified"
|
||||
date_published = "datePublished"
|
||||
discussion_url = "discussionUrl"
|
||||
edit_eidr = "editEIDR"
|
||||
editor = "editor"
|
||||
educational_alignment = "educationalAlignment"
|
||||
educational_level = "educationalLevel"
|
||||
educational_use = "educationalUse"
|
||||
encoding = "encoding"
|
||||
encoding_format = "encodingFormat"
|
||||
example_of_work = "exampleOfWork"
|
||||
expires = "expires"
|
||||
funder = "funder"
|
||||
genre = "genre"
|
||||
has_part = "hasPart"
|
||||
headline = "headline"
|
||||
in_language = "inLanguage"
|
||||
interaction_statistic = "interactionStatistic"
|
||||
interactivity_type = "interactivityType"
|
||||
interpreted_as_claim = "interpretedAsClaim"
|
||||
is_accessible_for_free = "isAccessibleForFree"
|
||||
is_based_on = "isBasedOn"
|
||||
is_family_friendly = "isFamilyFriendly"
|
||||
is_part_of = "isPartOf"
|
||||
keywords = "keywords"
|
||||
learning_resource_type = "learningResourceType"
|
||||
license = "license"
|
||||
location_created = "locationCreated"
|
||||
main_entity = "mainEntity"
|
||||
maintainer = "maintainer"
|
||||
material = "material"
|
||||
material_extent = "materialExtent"
|
||||
mentions = "mentions"
|
||||
offers = "offers"
|
||||
pattern = "pattern"
|
||||
position = "position"
|
||||
producer = "producer"
|
||||
provider = "provider"
|
||||
publication = "publication"
|
||||
publisher = "publisher"
|
||||
publisher_imprint = "publisherImprint"
|
||||
publishing_principles = "publishingPrinciples"
|
||||
recorded_at = "recordedAt"
|
||||
released_event = "releasedEvent"
|
||||
review = "review"
|
||||
schema_version = "schemaVersion"
|
||||
sd_date_published = "sdDatePublished"
|
||||
sd_license = "sdLicense"
|
||||
sd_publisher = "sdPublisher"
|
||||
size = "size"
|
||||
source_organization = "sourceOrganization"
|
||||
spatial = "spatial"
|
||||
spatial_coverage = "spatialCoverage"
|
||||
sponsor = "sponsor"
|
||||
teaches = "teaches"
|
||||
temporal = "temporal"
|
||||
temporal_coverage = "temporalCoverage"
|
||||
text = "text"
|
||||
thumbnail_url = "thumbnailUrl"
|
||||
time_required = "timeRequired"
|
||||
translation_of_work = "translationOfWork"
|
||||
translator = "translator"
|
||||
typical_age_range = "typicalAgeRange"
|
||||
usage_info = "usageInfo"
|
||||
version = "version"
|
||||
video = "video"
|
||||
work_example = "workExample"
|
||||
work_translation = "workTranslation"
|
||||
additional_type = "additionalType"
|
||||
alternate_name = "alternateName"
|
||||
description = "description"
|
||||
disambiguating_description = "disambiguatingDescription"
|
||||
identifier = "identifier"
|
||||
image = "image"
|
||||
main_entity_of_page = "mainEntityOfPage"
|
||||
name = "name"
|
||||
potential_action = "potentialAction"
|
||||
same_as = "sameAs"
|
||||
subject_of = "subjectOf"
|
||||
url = "url"
|
@ -1,2 +0,0 @@
|
||||
from .http_services import *
|
||||
from .router_factory import *
|
@ -1,156 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Callable, Generic, Type, TypeVar
|
||||
|
||||
from fastapi import BackgroundTasks, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.config import get_app_dirs, get_app_settings
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.db.db_setup import SessionLocal
|
||||
from mealie.lang import get_locale_provider
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
T = TypeVar("T")
|
||||
D = TypeVar("D")
|
||||
|
||||
|
||||
CLS_DEP = TypeVar("CLS_DEP") # Generic Used for the class method dependencies
|
||||
|
||||
|
||||
class BaseHttpService(Generic[T, D], ABC):
|
||||
"""
|
||||
The BaseHttpService class is a generic class that can be used to create
|
||||
http services that are injected via `Depends` into a route function. To use,
|
||||
you must define the Generic type arguments:
|
||||
|
||||
`T`: The type passed into the *_existing functions (e.g. id) which is then passed into assert_existing
|
||||
`D`: Item returned from database layer
|
||||
"""
|
||||
|
||||
item: D = None
|
||||
|
||||
# Function that Generate Corrsesponding Routes through RouterFactory:
|
||||
# if the method is defined or != `None` than the corresponding route is defined through the RouterFactory.
|
||||
# If the method is not defined, then the route will be excluded from creation. This service based articheture
|
||||
# is being adopted as apart of the v1 migration
|
||||
get_all: Callable = None
|
||||
create_one: Callable = None
|
||||
update_one: Callable = None
|
||||
update_many: Callable = None
|
||||
populate_item: Callable = None
|
||||
delete_one: Callable = None
|
||||
delete_all: Callable = None
|
||||
|
||||
# Type Definitions
|
||||
_schema = None
|
||||
|
||||
# Function called to create a server side event
|
||||
event_func: Callable = None
|
||||
|
||||
# Config
|
||||
_restrict_by_group = False
|
||||
_group_id_cache = None
|
||||
|
||||
def __init__(self, session: Session, user: PrivateUser, background_tasks: BackgroundTasks = None) -> None:
|
||||
self.session = session or SessionLocal()
|
||||
self.user = user
|
||||
self.logged_in = bool(self.user)
|
||||
self.background_tasks = background_tasks
|
||||
|
||||
# Static Globals Dependency Injection
|
||||
self.db = get_repositories(session)
|
||||
self.app_dirs = get_app_dirs()
|
||||
self.settings = get_app_settings()
|
||||
self.t = get_locale_provider().t
|
||||
|
||||
def _existing_factory(dependency: Type[CLS_DEP]) -> classmethod:
|
||||
def cls_method(cls, item_id: T, deps: CLS_DEP = Depends(dependency)):
|
||||
new_class = cls(session=deps.session, user=deps.user, background_tasks=deps.bg_task)
|
||||
new_class.assert_existing(item_id)
|
||||
return new_class
|
||||
|
||||
return classmethod(cls_method)
|
||||
|
||||
def _class_method_factory(dependency: Type[CLS_DEP]) -> classmethod:
|
||||
def cls_method(cls, deps: CLS_DEP = Depends(dependency)):
|
||||
return cls(session=deps.session, user=deps.user, background_tasks=deps.bg_task)
|
||||
|
||||
return classmethod(cls_method)
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def public(cls, deps: Any):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def private(cls, deps: Any):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def read_existing(cls, deps: Any):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def write_existing(cls, deps: Any):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def populate_item(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def group_id(self):
|
||||
# TODO: Populate Group in Private User Call WARNING: May require significant refactoring
|
||||
if not self._group_id_cache:
|
||||
group = self.db.groups.get_one(self.user.group, "name")
|
||||
self._group_id_cache = group.id
|
||||
return self._group_id_cache
|
||||
|
||||
def cast(self, item: BaseModel, dest, assign_owner=True) -> T:
|
||||
"""cast a pydantic model to the destination type
|
||||
|
||||
Args:
|
||||
item (BaseModel): A pydantic model containing data
|
||||
dest ([type]): A type to cast the data to
|
||||
assign_owner (bool, optional): If true, will assign the user_id and group_id to the dest type. Defaults to True.
|
||||
|
||||
Returns:
|
||||
TypeVar(dest): Returns the destionation model type
|
||||
"""
|
||||
data = item.dict()
|
||||
|
||||
if assign_owner:
|
||||
data["user_id"] = self.user.id
|
||||
data["group_id"] = self.group_id
|
||||
|
||||
return dest(**data)
|
||||
|
||||
def assert_existing(self, id: T) -> None:
|
||||
self.populate_item(id)
|
||||
self._check_item()
|
||||
|
||||
def _check_item(self) -> None:
|
||||
if not self.item:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if self.__class__._restrict_by_group:
|
||||
group_id = getattr(self.item, "group_id", False)
|
||||
|
||||
if not group_id or group_id != self.group_id:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
if hasattr(self, "check_item"):
|
||||
self.check_item()
|
||||
|
||||
def _create_event(self, title: str, message: str) -> None:
|
||||
if not self.__class__.event_func:
|
||||
raise NotImplementedError("`event_func` must be set by child class")
|
||||
|
||||
self.background_tasks.add_task(self.__class__.event_func, title, message, self.session)
|
@ -1,83 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from mealie.core.root_logger import get_logger
|
||||
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)
|
||||
DAL = TypeVar("DAL", bound=RepositoryGeneric)
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class CrudHttpMixins(Generic[C, R, U], ABC):
|
||||
item: R
|
||||
session: Session
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def repo(self) -> DAL:
|
||||
...
|
||||
|
||||
def populate_item(self, id: int) -> R:
|
||||
self.item = self.repo.get_one(id)
|
||||
return self.item
|
||||
|
||||
def _create_one(self, data: C, default_msg="generic-create-error", exception_msgs: dict | None = None) -> R:
|
||||
try:
|
||||
self.item = self.repo.create(data)
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
self.session.rollback()
|
||||
|
||||
msg = default_msg
|
||||
if exception_msgs:
|
||||
msg = exception_msgs.get(type(ex), default_msg)
|
||||
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorResponse(
|
||||
message=msg,
|
||||
exception=str(ex),
|
||||
).dict(),
|
||||
)
|
||||
|
||||
return self.item
|
||||
|
||||
def _update_one(self, data: U, item_id: int = None) -> R:
|
||||
if not self.item:
|
||||
return
|
||||
|
||||
target_id = item_id or self.item.id
|
||||
self.item = self.repo.update(target_id, data)
|
||||
|
||||
return self.item
|
||||
|
||||
def _patch_one(self, data: U, item_id: int) -> None:
|
||||
try:
|
||||
self.item = self.repo.patch(item_id, data.dict(exclude_unset=True, exclude_defaults=True))
|
||||
except IntegrityError:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "generic-patch-error"})
|
||||
|
||||
def _delete_one(self, item_id: int = None) -> R:
|
||||
target_id = item_id or self.item.id
|
||||
logger.info(f"Deleting item with id {target_id}")
|
||||
|
||||
try:
|
||||
self.item = self.repo.delete(target_id)
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST, detail={"message": "generic-delete-error", "exception": str(ex)}
|
||||
)
|
||||
|
||||
return self.item
|
@ -1,60 +0,0 @@
|
||||
from abc import abstractmethod
|
||||
from typing import TypeVar
|
||||
|
||||
from mealie.core.dependencies.grouped import AdminDeps, PublicDeps, UserDeps
|
||||
|
||||
from .base_http_service import BaseHttpService
|
||||
|
||||
T = TypeVar("T")
|
||||
D = TypeVar("D")
|
||||
|
||||
|
||||
class PublicHttpService(BaseHttpService[T, D]):
|
||||
"""
|
||||
PublicHttpService sets the class methods to PublicDeps for read actions
|
||||
and UserDeps for write actions which are inaccessible to not logged in users.
|
||||
"""
|
||||
|
||||
read_existing = BaseHttpService._existing_factory(PublicDeps)
|
||||
write_existing = BaseHttpService._existing_factory(UserDeps)
|
||||
|
||||
public = BaseHttpService._class_method_factory(PublicDeps)
|
||||
private = BaseHttpService._class_method_factory(UserDeps)
|
||||
|
||||
@abstractmethod
|
||||
def populate_item(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
class UserHttpService(BaseHttpService[T, D]):
|
||||
"""
|
||||
UserHttpService sets the class methods to UserDeps which are inaccessible
|
||||
to not logged in users.
|
||||
"""
|
||||
|
||||
read_existing = BaseHttpService._existing_factory(UserDeps)
|
||||
write_existing = BaseHttpService._existing_factory(UserDeps)
|
||||
|
||||
public = BaseHttpService._class_method_factory(UserDeps)
|
||||
private = BaseHttpService._class_method_factory(UserDeps)
|
||||
|
||||
@abstractmethod
|
||||
def populate_item(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
class AdminHttpService(BaseHttpService[T, D]):
|
||||
"""
|
||||
AdminHttpService restricts the class methods to AdminDeps which are restricts
|
||||
all class methods to users who are administrators.
|
||||
"""
|
||||
|
||||
read_existing = BaseHttpService._existing_factory(AdminDeps)
|
||||
write_existing = BaseHttpService._existing_factory(AdminDeps)
|
||||
|
||||
public = BaseHttpService._class_method_factory(AdminDeps)
|
||||
private = BaseHttpService._class_method_factory(AdminDeps)
|
||||
|
||||
@abstractmethod
|
||||
def populate_item(self) -> None:
|
||||
...
|
@ -1,214 +0,0 @@
|
||||
import inspect
|
||||
from typing import Any, Callable, Optional, Sequence, Type, TypeVar, get_type_hints
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.params import Depends
|
||||
from fastapi.types import DecoratedCallable
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .base_http_service import BaseHttpService
|
||||
|
||||
"""
|
||||
This code is largely based off of the FastAPI Crud Router
|
||||
https://github.com/awtkns/fastapi-crudrouter/blob/master/fastapi_crudrouter/core/_base.py
|
||||
"""
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
S = TypeVar("S", bound=BaseHttpService)
|
||||
DEPENDENCIES = Optional[Sequence[Depends]]
|
||||
|
||||
|
||||
def get_return(func: Callable, default) -> Type:
|
||||
return get_type_hints(func).get("return", default)
|
||||
|
||||
|
||||
def get_func_args(func: Callable) -> Sequence[str]:
|
||||
for _, value in get_type_hints(func).items():
|
||||
if value:
|
||||
return value
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class RouterFactory(APIRouter):
|
||||
|
||||
schema: Type[T]
|
||||
_base_path: str = "/"
|
||||
|
||||
def __init__(self, service: Type[S], prefix: Optional[str] = None, tags: Optional[list[str]] = None, *_, **kwargs):
|
||||
"""
|
||||
RouterFactory takes a concrete service class derived from the BaseHttpService class and returns common
|
||||
CRUD Routes for the service. The following features are implmeneted in the RouterFactory:
|
||||
|
||||
1. API endpoint Descriptions are read from the docstrings of the methods in the passed in service class
|
||||
2. Return types are inferred from the concrete service schema, or specified from the return type annotations.
|
||||
This provides flexibility to return different types based on each route depending on client needs.
|
||||
3. Arguemnt types are inferred for Post and Put routes where the first type annotated argument is the data that
|
||||
is beging posted or updated. Note that this is only done for the first argument of the method.
|
||||
4. The Get and Delete routes assume that you've defined the `write_existing` and `read_existing` methods in the
|
||||
service class. The dependencies defined in the `write_existing` and `read_existing` methods are passed directly
|
||||
to the FastAPI router and as such should include the `item_id` or equilivent argument.
|
||||
"""
|
||||
self.service: Type[S] = service
|
||||
self.schema: Type[T] = service._schema
|
||||
|
||||
prefix = str(prefix or self.schema.__name__).lower()
|
||||
prefix = self._base_path + prefix.strip("/")
|
||||
tags = tags or [prefix.strip("/").capitalize()]
|
||||
|
||||
super().__init__(prefix=prefix, tags=tags, **kwargs)
|
||||
|
||||
if self.service.get_all:
|
||||
self._add_api_route(
|
||||
"",
|
||||
self._get_all(),
|
||||
methods=["GET"],
|
||||
response_model=Optional[list[self.schema]], # type: ignore
|
||||
summary="Get All",
|
||||
description=inspect.cleandoc(self.service.get_all.__doc__ or ""),
|
||||
)
|
||||
|
||||
if self.service.create_one:
|
||||
self._add_api_route(
|
||||
"",
|
||||
self._create(),
|
||||
methods=["POST"],
|
||||
response_model=self.schema,
|
||||
summary="Create One",
|
||||
status_code=201,
|
||||
description=inspect.cleandoc(self.service.create_one.__doc__ or ""),
|
||||
)
|
||||
|
||||
if self.service.update_many:
|
||||
self._add_api_route(
|
||||
"",
|
||||
self._update_many(),
|
||||
methods=["PUT"],
|
||||
response_model=Optional[list[self.schema]], # type: ignore
|
||||
summary="Update Many",
|
||||
description=inspect.cleandoc(self.service.update_many.__doc__ or ""),
|
||||
)
|
||||
|
||||
if self.service.delete_all:
|
||||
self._add_api_route(
|
||||
"",
|
||||
self._delete_all(),
|
||||
methods=["DELETE"],
|
||||
response_model=Optional[list[self.schema]], # type: ignore
|
||||
summary="Delete All",
|
||||
description=inspect.cleandoc(self.service.delete_all.__doc__ or ""),
|
||||
)
|
||||
|
||||
if self.service.populate_item:
|
||||
self._add_api_route(
|
||||
"/{item_id}",
|
||||
self._get_one(),
|
||||
methods=["GET"],
|
||||
response_model=get_type_hints(self.service.populate_item).get("return", self.schema),
|
||||
summary="Get One",
|
||||
description=inspect.cleandoc(self.service.populate_item.__doc__ or ""),
|
||||
)
|
||||
|
||||
if self.service.update_one:
|
||||
self._add_api_route(
|
||||
"/{item_id}",
|
||||
self._update(),
|
||||
methods=["PUT"],
|
||||
response_model=self.schema,
|
||||
summary="Update One",
|
||||
description=inspect.cleandoc(self.service.update_one.__doc__ or ""),
|
||||
)
|
||||
|
||||
if self.service.delete_one:
|
||||
self._add_api_route(
|
||||
"/{item_id}",
|
||||
self._delete_one(),
|
||||
methods=["DELETE"],
|
||||
response_model=self.schema,
|
||||
summary="Delete One",
|
||||
description=inspect.cleandoc(self.service.delete_one.__doc__ or ""),
|
||||
)
|
||||
|
||||
def _add_api_route(self, path: str, endpoint: Callable[..., Any], **kwargs: Any) -> None:
|
||||
dependencies = []
|
||||
super().add_api_route(path, endpoint, dependencies=dependencies, **kwargs)
|
||||
|
||||
def api_route(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
"""Overrides and exiting route if it exists"""
|
||||
methods = kwargs["methods"] if "methods" in kwargs else ["GET"]
|
||||
self.remove_api_route(path, methods)
|
||||
return super().api_route(path, *args, **kwargs)
|
||||
|
||||
def get(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
self.remove_api_route(path, ["Get"])
|
||||
return super().get(path, *args, **kwargs)
|
||||
|
||||
def post(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
self.remove_api_route(path, ["POST"])
|
||||
return super().post(path, *args, **kwargs)
|
||||
|
||||
def put(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
self.remove_api_route(path, ["PUT"])
|
||||
return super().put(path, *args, **kwargs)
|
||||
|
||||
def delete(self, path: str, *args: Any, **kwargs: Any) -> Callable[[DecoratedCallable], DecoratedCallable]:
|
||||
self.remove_api_route(path, ["DELETE"])
|
||||
return super().delete(path, *args, **kwargs)
|
||||
|
||||
def remove_api_route(self, path: str, methods: list[str]) -> None:
|
||||
methods_ = set(methods)
|
||||
|
||||
for route in self.routes:
|
||||
if route.path == f"{self.prefix}{path}" and route.methods == methods_:
|
||||
self.routes.remove(route)
|
||||
|
||||
def _get_all(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
service_dep = getattr(self.service, "get_all_dep", self.service.private)
|
||||
|
||||
def route(service: S = Depends(service_dep)) -> T: # type: ignore
|
||||
return service.get_all()
|
||||
|
||||
return route
|
||||
|
||||
def _get_one(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
def route(service: S = Depends(self.service.write_existing)) -> T: # type: ignore
|
||||
return service.item
|
||||
|
||||
return route
|
||||
|
||||
def _create(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
create_schema = get_func_args(self.service.create_one) or self.schema
|
||||
|
||||
def route(data: create_schema, service: S = Depends(self.service.private)) -> T: # type: ignore
|
||||
return service.create_one(data)
|
||||
|
||||
return route
|
||||
|
||||
def _update(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
update_schema = get_func_args(self.service.update_one) or self.schema
|
||||
|
||||
def route(data: update_schema, service: S = Depends(self.service.write_existing)) -> T: # type: ignore
|
||||
return service.update_one(data)
|
||||
|
||||
return route
|
||||
|
||||
def _update_many(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
update_many_schema = get_func_args(self.service.update_many) or list[self.schema]
|
||||
|
||||
def route(data: update_many_schema, service: S = Depends(self.service.private)) -> T: # type: ignore
|
||||
return service.update_many(data)
|
||||
|
||||
return route
|
||||
|
||||
def _delete_one(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
def route(service: S = Depends(self.service.write_existing)) -> T: # type: ignore
|
||||
return service.delete_one()
|
||||
|
||||
return route
|
||||
|
||||
def _delete_all(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def get_routes() -> list[str]:
|
||||
return ["get_all", "create", "delete_all", "get_one", "update", "delete_one"]
|
@ -1,9 +1,11 @@
|
||||
from mealie.core.config import get_app_dirs, get_app_settings
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.lang import get_locale_provider
|
||||
|
||||
|
||||
class BaseService:
|
||||
def __init__(self) -> None:
|
||||
self.app_dirs = get_app_dirs()
|
||||
self.directories = get_app_dirs()
|
||||
self.settings = get_app_settings()
|
||||
self.t = get_locale_provider()
|
||||
self.logger = get_logger()
|
||||
|
@ -1 +0,0 @@
|
||||
from .backup_service import *
|
@ -1,62 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.schema.group.group import GroupAdminUpdate
|
||||
from mealie.schema.mapper import mapper
|
||||
from mealie.schema.response import ErrorResponse
|
||||
from mealie.schema.user.user import GroupBase, GroupInDB
|
||||
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
|
||||
from mealie.services._base_http_service.http_services import AdminHttpService
|
||||
from mealie.services.events import create_group_event
|
||||
from mealie.services.group_services.group_utils import create_new_group
|
||||
|
||||
|
||||
class AdminGroupService(
|
||||
CrudHttpMixins[GroupBase, GroupInDB, GroupAdminUpdate],
|
||||
AdminHttpService[UUID4, GroupInDB],
|
||||
):
|
||||
event_func = create_group_event
|
||||
_schema = GroupInDB
|
||||
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.db.groups
|
||||
|
||||
def populate_item(self, id: UUID4) -> GroupInDB:
|
||||
self.item = self.repo.get_one(id)
|
||||
return self.item
|
||||
|
||||
def get_all(self) -> list[GroupInDB]:
|
||||
return self.repo.get_all()
|
||||
|
||||
def create_one(self, data: GroupBase) -> GroupInDB:
|
||||
return create_new_group(self.db, data)
|
||||
|
||||
def update_one(self, data: GroupAdminUpdate, item_id: UUID4 = None) -> GroupInDB:
|
||||
target_id = item_id or data.id
|
||||
|
||||
if data.preferences:
|
||||
preferences = self.db.group_preferences.get_one(value=target_id, key="group_id")
|
||||
preferences = mapper(data.preferences, preferences)
|
||||
self.item.preferences = self.db.group_preferences.update(target_id, preferences)
|
||||
|
||||
if data.name not in ["", self.item.name]:
|
||||
self.item.name = data.name
|
||||
self.item = self.repo.update(target_id, self.item)
|
||||
|
||||
return self.item
|
||||
|
||||
def delete_one(self, id: UUID4 = None) -> GroupInDB:
|
||||
target_id = id or self.item.id
|
||||
|
||||
if len(self.item.users) > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorResponse(message="Cannot delete group with users").dict(),
|
||||
)
|
||||
|
||||
return self._delete_one(target_id)
|
@ -1,36 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
from mealie.schema.user.user import UserIn, UserOut
|
||||
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
|
||||
from mealie.services._base_http_service.http_services import AdminHttpService
|
||||
from mealie.services.events import create_user_event
|
||||
|
||||
|
||||
class AdminUserService(
|
||||
CrudHttpMixins[UserOut, UserIn, UserIn],
|
||||
AdminHttpService[int, UserOut],
|
||||
):
|
||||
event_func = create_user_event
|
||||
_schema = UserOut
|
||||
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.db.users
|
||||
|
||||
def populate_item(self, id: int) -> UserOut:
|
||||
self.item = self.repo.get_one(id)
|
||||
return self.item
|
||||
|
||||
def get_all(self) -> list[UserOut]:
|
||||
return self.repo.get_all()
|
||||
|
||||
def create_one(self, data: UserIn) -> UserOut:
|
||||
return self._create_one(data)
|
||||
|
||||
def update_one(self, data: UserOut, item_id: int = None) -> UserOut:
|
||||
return self._update_one(data, item_id)
|
||||
|
||||
def delete_one(self, id: int = None) -> UserOut:
|
||||
return self._delete_one(id)
|
@ -1,36 +0,0 @@
|
||||
import operator
|
||||
|
||||
from mealie.schema.admin.backup import AllBackups, BackupFile, CreateBackup
|
||||
from mealie.services._base_http_service import AdminHttpService
|
||||
from mealie.services.events import create_backup_event
|
||||
|
||||
from .exporter import Exporter
|
||||
|
||||
|
||||
class BackupHttpService(AdminHttpService):
|
||||
event_func = create_backup_event
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.exporter = Exporter()
|
||||
|
||||
def get_all(self) -> AllBackups:
|
||||
imports = []
|
||||
for archive in self.app_dirs.BACKUP_DIR.glob("*.zip"):
|
||||
backup = BackupFile(name=archive.name, date=archive.stat().st_ctime)
|
||||
imports.append(backup)
|
||||
|
||||
templates = [template.name for template in self.app_dirs.TEMPLATE_DIR.glob("*.*")]
|
||||
imports.sort(key=operator.attrgetter("date"), reverse=True)
|
||||
|
||||
return AllBackups(imports=imports, templates=templates)
|
||||
|
||||
def create_one(self, options: CreateBackup):
|
||||
pass
|
||||
|
||||
def delete_one(self):
|
||||
pass
|
||||
|
||||
|
||||
class BackupService:
|
||||
pass
|
@ -1,2 +0,0 @@
|
||||
class Exporter:
|
||||
pass
|
@ -1,21 +0,0 @@
|
||||
from mealie.services._base_http_service import AdminHttpService
|
||||
|
||||
from .importer import Importer
|
||||
|
||||
|
||||
class ImportHttpService(AdminHttpService):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.exporter = Importer()
|
||||
|
||||
def get_all(self):
|
||||
pass
|
||||
|
||||
def get_one(self):
|
||||
pass
|
||||
|
||||
def create(self):
|
||||
pass
|
||||
|
||||
def delete_one(self):
|
||||
pass
|
@ -1,2 +0,0 @@
|
||||
class Importer:
|
||||
pass
|
@ -288,28 +288,15 @@ def import_database(
|
||||
import_settings=True,
|
||||
import_users=True,
|
||||
import_groups=True,
|
||||
import_notifications=True,
|
||||
force_import: bool = False,
|
||||
rebase: bool = False,
|
||||
**_,
|
||||
):
|
||||
import_session = ImportDatabase(user, session, archive, force_import)
|
||||
|
||||
recipe_report = []
|
||||
if import_recipes:
|
||||
recipe_report = import_session.import_recipes()
|
||||
|
||||
settings_report = []
|
||||
if import_settings:
|
||||
settings_report = import_session.import_settings()
|
||||
|
||||
group_report = []
|
||||
if import_groups:
|
||||
group_report = import_session.import_groups()
|
||||
|
||||
user_report = []
|
||||
if import_users:
|
||||
user_report = import_session.import_users()
|
||||
|
||||
recipe_report = import_session.import_recipes() if import_recipes else []
|
||||
settings_report = import_session.import_settings() if import_settings else []
|
||||
group_report = import_session.import_groups() if import_groups else []
|
||||
user_report = import_session.import_users() if import_users else []
|
||||
notification_report = []
|
||||
|
||||
import_session.clean_up()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user