mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-06-19 15:30:16 -04:00
feat: Add Households to Mealie (#3970)
This commit is contained in:
@@ -68,7 +68,7 @@ async def get_public_group(group_slug: str = fastapi.Path(...), session=Depends(
|
||||
repos = get_repositories(session)
|
||||
group = repos.groups.get_by_slug_or_id(group_slug)
|
||||
|
||||
if not group or group.preferences.private_group or not group.preferences.recipe_public:
|
||||
if not group or group.preferences.private_group:
|
||||
raise HTTPException(404, "group not found")
|
||||
else:
|
||||
return group
|
||||
@@ -111,7 +111,7 @@ async def get_current_user(
|
||||
except PyJWTError as e:
|
||||
raise credentials_exception from e
|
||||
|
||||
repos = get_repositories(session)
|
||||
repos = get_repositories(session, group_id=None, household_id=None)
|
||||
|
||||
user = repos.users.get_one(token_data.user_id, "id", any_case=False)
|
||||
|
||||
@@ -139,7 +139,7 @@ async def get_admin_user(current_user: PrivateUser = Depends(get_current_user))
|
||||
|
||||
|
||||
def validate_long_live_token(session: Session, client_token: str, user_id: str) -> PrivateUser:
|
||||
repos = get_repositories(session)
|
||||
repos = get_repositories(session, group_id=None, household_id=None)
|
||||
|
||||
token = repos.api_tokens.multi_query({"token": client_token, "user_id": user_id})
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class AuthProvider(Generic[T], metaclass=abc.ABCMeta):
|
||||
if self.__has_tried_user:
|
||||
return self.user
|
||||
|
||||
db = get_repositories(self.session)
|
||||
db = get_repositories(self.session, group_id=None, household_id=None)
|
||||
|
||||
user = user = db.users.get_one(username, "username", any_case=True)
|
||||
if not user:
|
||||
|
||||
@@ -23,7 +23,7 @@ class CredentialsProvider(AuthProvider[CredentialsRequest]):
|
||||
async def authenticate(self) -> tuple[str, timedelta] | None:
|
||||
"""Attempt to authenticate a user given a username and password"""
|
||||
settings = get_app_settings()
|
||||
db = get_repositories(self.session)
|
||||
db = get_repositories(self.session, group_id=None, household_id=None)
|
||||
user = self.try_get_user(self.data.username)
|
||||
|
||||
if not user:
|
||||
|
||||
@@ -95,7 +95,7 @@ class LDAPProvider(CredentialsProvider):
|
||||
"""
|
||||
|
||||
settings = get_app_settings()
|
||||
db = get_repositories(self.session)
|
||||
db = get_repositories(self.session, group_id=None, household_id=None)
|
||||
if not self.data:
|
||||
return None
|
||||
data = self.data
|
||||
|
||||
@@ -33,7 +33,7 @@ class OpenIDProvider(AuthProvider[OIDCRequest]):
|
||||
if not claims:
|
||||
return None
|
||||
|
||||
repos = get_repositories(self.session)
|
||||
repos = get_repositories(self.session, group_id=None, household_id=None)
|
||||
|
||||
user = self.try_get_user(claims.get(settings.OIDC_USER_CLAIM))
|
||||
is_admin = False
|
||||
|
||||
@@ -179,6 +179,7 @@ class AppSettings(AppLoggingSettings):
|
||||
return self.DB_PROVIDER.db_url_public if self.DB_PROVIDER else None
|
||||
|
||||
DEFAULT_GROUP: str = "Home"
|
||||
DEFAULT_HOUSEHOLD: str = "Family"
|
||||
|
||||
_DEFAULT_EMAIL: str = "changeme@example.com"
|
||||
"""
|
||||
|
||||
@@ -26,7 +26,7 @@ SessionLocal, engine = sql_global_init(settings.DB_URL) # type: ignore
|
||||
|
||||
|
||||
@contextmanager
|
||||
def session_context() -> Session:
|
||||
def session_context() -> Generator[Session, None, None]:
|
||||
"""
|
||||
session_context() provides a managed session to the database that is automatically
|
||||
closed when the context is exited. This is the preferred method of accessing the
|
||||
|
||||
@@ -5,7 +5,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from mealie.core import root_logger
|
||||
from mealie.db.models.group.group import Group
|
||||
from mealie.db.models.group.shopping_list import ShoppingList, ShoppingListMultiPurposeLabel
|
||||
from mealie.db.models.household.shopping_list import ShoppingList, ShoppingListMultiPurposeLabel
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
|
||||
@@ -17,25 +17,38 @@ from mealie.db.fixes.fix_slug_foods import fix_slug_food_names
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.repos.repository_factory import AllRepositories
|
||||
from mealie.repos.seed.init_users import default_user_init
|
||||
from mealie.schema.user.user import GroupBase
|
||||
from mealie.schema.household.household import HouseholdCreate, HouseholdInDB
|
||||
from mealie.schema.user.user import GroupBase, GroupInDB
|
||||
from mealie.services.group_services.group_service import GroupService
|
||||
from mealie.services.household_services.household_service import HouseholdService
|
||||
|
||||
PROJECT_DIR = Path(__file__).parent.parent.parent
|
||||
|
||||
logger = root_logger.get_logger()
|
||||
|
||||
|
||||
def init_db(db: AllRepositories) -> None:
|
||||
default_group_init(db)
|
||||
default_user_init(db)
|
||||
|
||||
|
||||
def default_group_init(db: AllRepositories):
|
||||
def init_db(session: orm.Session) -> None:
|
||||
settings = get_app_settings()
|
||||
|
||||
logger.info("Generating Default Group")
|
||||
instance_repos = get_repositories(session)
|
||||
default_group = default_group_init(instance_repos, settings.DEFAULT_GROUP)
|
||||
|
||||
GroupService.create_group(db, GroupBase(name=settings.DEFAULT_GROUP))
|
||||
group_repos = get_repositories(session, group_id=default_group.id, household_id=None)
|
||||
default_household = group_repos.households.get_by_name(settings.DEFAULT_HOUSEHOLD)
|
||||
if default_household is None:
|
||||
default_household = default_household_init(group_repos, settings.DEFAULT_HOUSEHOLD)
|
||||
household_repos = get_repositories(session, group_id=default_group.id, household_id=default_household.id)
|
||||
default_user_init(household_repos)
|
||||
|
||||
|
||||
def default_group_init(repos: AllRepositories, name: str) -> GroupInDB:
|
||||
logger.info("Generating Default Group and Household")
|
||||
return GroupService.create_group(repos, GroupBase(name=name))
|
||||
|
||||
|
||||
def default_household_init(repos: AllRepositories, name: str) -> HouseholdInDB:
|
||||
logger.info("Generating Default Household")
|
||||
return HouseholdService.create_household(repos, HouseholdCreate(name=name))
|
||||
|
||||
|
||||
# Adapted from https://alembic.sqlalchemy.org/en/latest/cookbook.html#test-current-database-revision-is-at-head-s
|
||||
@@ -103,7 +116,7 @@ def main():
|
||||
if session.get_bind().name == "postgresql": # needed for fuzzy search and fast GIN text indices
|
||||
session.execute(text("CREATE EXTENSION IF NOT EXISTS pg_trgm;"))
|
||||
|
||||
db = get_repositories(session)
|
||||
db = get_repositories(session, group_id=None, household_id=None)
|
||||
safe_try(lambda: fix_migration_data(session))
|
||||
safe_try(lambda: fix_slug_food_names(db))
|
||||
safe_try(lambda: fix_group_with_no_name(session))
|
||||
@@ -112,7 +125,7 @@ def main():
|
||||
logger.debug("Database exists")
|
||||
else:
|
||||
logger.info("Database contains no users, initializing...")
|
||||
init_db(db)
|
||||
init_db(session)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Integer
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, declared_attr, mapped_column, synonym
|
||||
from text_unidecode import unidecode
|
||||
|
||||
from ._model_utils.datetime import get_utc_now
|
||||
@@ -12,6 +12,10 @@ class SqlAlchemyBase(DeclarativeBase):
|
||||
created_at: Mapped[datetime | None] = mapped_column(DateTime, default=get_utc_now, index=True)
|
||||
update_at: Mapped[datetime | None] = mapped_column(DateTime, default=get_utc_now, onupdate=get_utc_now)
|
||||
|
||||
@declared_attr
|
||||
def updated_at(cls) -> Mapped[datetime | None]:
|
||||
return synonym("update_at")
|
||||
|
||||
@classmethod
|
||||
def normalize(cls, val: str) -> str:
|
||||
return unidecode(val).lower().strip()
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
from .cookbook import *
|
||||
from .events import *
|
||||
from .exports import *
|
||||
from .group import *
|
||||
from .invite_tokens import *
|
||||
from .mealplan import *
|
||||
from .preferences import *
|
||||
from .recipe_action import *
|
||||
from .report import *
|
||||
from .shopping_list import *
|
||||
from .webhooks import *
|
||||
|
||||
@@ -1,40 +1,32 @@
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.config import get_app_settings
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
from ..group.invite_tokens import GroupInviteToken
|
||||
from ..group.webhooks import GroupWebhooksModel
|
||||
from ..household.cookbook import CookBook
|
||||
from ..household.invite_tokens import GroupInviteToken
|
||||
from ..household.mealplan import GroupMealPlan
|
||||
from ..household.webhooks import GroupWebhooksModel
|
||||
from ..recipe.category import Category, group_to_categories
|
||||
from ..server.task import ServerTaskModel
|
||||
from .cookbook import CookBook
|
||||
from .mealplan import GroupMealPlan
|
||||
from .preferences import GroupPreferencesModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..recipe import (
|
||||
IngredientFoodModel,
|
||||
IngredientUnitModel,
|
||||
RecipeModel,
|
||||
Tag,
|
||||
Tool,
|
||||
)
|
||||
from ..household import Household
|
||||
from ..household.events import GroupEventNotifierModel
|
||||
from ..household.recipe_action import GroupRecipeAction
|
||||
from ..household.shopping_list import ShoppingList
|
||||
from ..recipe import IngredientFoodModel, IngredientUnitModel, RecipeModel, Tag, Tool
|
||||
from ..users import User
|
||||
from .events import GroupEventNotifierModel
|
||||
from .exports import GroupDataExportsModel
|
||||
from .recipe_action import GroupRecipeAction
|
||||
from .report import ReportModel
|
||||
from .shopping_list import ShoppingList
|
||||
|
||||
|
||||
class Group(SqlAlchemyBase, BaseMixins):
|
||||
@@ -42,6 +34,7 @@ class Group(SqlAlchemyBase, BaseMixins):
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name: Mapped[str] = mapped_column(sa.String, index=True, nullable=False, unique=True)
|
||||
slug: Mapped[str | None] = mapped_column(sa.String, index=True, unique=True)
|
||||
households: Mapped[list["Household"]] = orm.relationship("Household", back_populates="group")
|
||||
users: Mapped[list["User"]] = orm.relationship("User", back_populates="group")
|
||||
categories: Mapped[list[Category]] = orm.relationship(Category, secondary=group_to_categories, single_parent=True)
|
||||
|
||||
@@ -89,6 +82,7 @@ class Group(SqlAlchemyBase, BaseMixins):
|
||||
tags: Mapped[list["Tag"]] = orm.relationship("Tag", **common_args)
|
||||
model_config = ConfigDict(
|
||||
exclude={
|
||||
"households",
|
||||
"users",
|
||||
"webhooks",
|
||||
"recipe_actions",
|
||||
@@ -104,12 +98,3 @@ class Group(SqlAlchemyBase, BaseMixins):
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod # TODO: Remove this
|
||||
def get_by_name(session: Session, name: str) -> Optional["Group"]:
|
||||
settings = get_app_settings()
|
||||
|
||||
item = session.execute(select(Group).filter(Group.name == name)).scalars().one_or_none()
|
||||
if item is None:
|
||||
item = session.execute(select(Group).filter(Group.name == settings.DEFAULT_GROUP)).scalars().one_or_none()
|
||||
return item
|
||||
|
||||
@@ -20,9 +20,9 @@ class GroupPreferencesModel(SqlAlchemyBase, BaseMixins):
|
||||
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="preferences")
|
||||
|
||||
private_group: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||
first_day_of_week: Mapped[int | None] = mapped_column(sa.Integer, default=0)
|
||||
|
||||
# Recipe Defaults
|
||||
# Deprecated (see household preferences)
|
||||
first_day_of_week: Mapped[int | None] = mapped_column(sa.Integer, default=0)
|
||||
recipe_public: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||
recipe_show_nutrition: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
recipe_show_assets: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
|
||||
35
mealie/db/models/household/__init__.py
Normal file
35
mealie/db/models/household/__init__.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from .cookbook import CookBook
|
||||
from .events import GroupEventNotifierModel, GroupEventNotifierOptionsModel
|
||||
from .household import Household
|
||||
from .invite_tokens import GroupInviteToken
|
||||
from .mealplan import GroupMealPlan, GroupMealPlanRules
|
||||
from .preferences import HouseholdPreferencesModel
|
||||
from .recipe_action import GroupRecipeAction
|
||||
from .shopping_list import (
|
||||
ShoppingList,
|
||||
ShoppingListExtras,
|
||||
ShoppingListItem,
|
||||
ShoppingListItemRecipeReference,
|
||||
ShoppingListMultiPurposeLabel,
|
||||
ShoppingListRecipeReference,
|
||||
)
|
||||
from .webhooks import GroupWebhooksModel
|
||||
|
||||
__all__ = [
|
||||
"CookBook",
|
||||
"GroupEventNotifierModel",
|
||||
"GroupEventNotifierOptionsModel",
|
||||
"GroupInviteToken",
|
||||
"GroupMealPlan",
|
||||
"GroupMealPlanRules",
|
||||
"Household",
|
||||
"HouseholdPreferencesModel",
|
||||
"GroupRecipeAction",
|
||||
"ShoppingList",
|
||||
"ShoppingListExtras",
|
||||
"ShoppingListItem",
|
||||
"ShoppingListItemRecipeReference",
|
||||
"ShoppingListMultiPurposeLabel",
|
||||
"ShoppingListRecipeReference",
|
||||
"GroupWebhooksModel",
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from sqlalchemy import Boolean, ForeignKey, Integer, String, orm
|
||||
from sqlalchemy import Boolean, ForeignKey, Integer, String, UniqueConstraint, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
@@ -11,16 +11,23 @@ from ..recipe.tag import Tag, cookbooks_to_tags
|
||||
from ..recipe.tool import Tool, cookbooks_to_tools
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .group import Group
|
||||
from ..group import Group
|
||||
from .household import Household
|
||||
|
||||
|
||||
class CookBook(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "cookbooks"
|
||||
__table_args__: tuple[UniqueConstraint, ...] = (
|
||||
UniqueConstraint("slug", "group_id", name="cookbook_slug_group_id_key"),
|
||||
)
|
||||
|
||||
id: Mapped[guid.GUID] = mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
|
||||
group_id: Mapped[guid.GUID | None] = mapped_column(guid.GUID, ForeignKey("groups.id"), index=True)
|
||||
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="cookbooks")
|
||||
household_id: Mapped[guid.GUID | None] = mapped_column(guid.GUID, ForeignKey("households.id"), index=True)
|
||||
household: Mapped[Optional["Household"]] = orm.relationship("Household", back_populates="cookbooks")
|
||||
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
slug: Mapped[str] = mapped_column(String, nullable=False, index=True)
|
||||
@@ -8,7 +8,8 @@ from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .group import Group
|
||||
from ..group import Group
|
||||
from .household import Household
|
||||
|
||||
|
||||
class GroupEventNotifierOptionsModel(SqlAlchemyBase, BaseMixins):
|
||||
@@ -62,6 +63,10 @@ class GroupEventNotifierModel(SqlAlchemyBase, BaseMixins):
|
||||
"Group", back_populates="group_event_notifiers", single_parent=True
|
||||
)
|
||||
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), index=True)
|
||||
household: Mapped[Optional["Household"]] = orm.relationship(
|
||||
"Household", back_populates="group_event_notifiers", single_parent=True
|
||||
)
|
||||
household_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("households.id"), index=True)
|
||||
|
||||
options: Mapped[GroupEventNotifierOptionsModel] = orm.relationship(
|
||||
GroupEventNotifierOptionsModel, uselist=False, cascade="all, delete-orphan"
|
||||
80
mealie/db/models/household/household.py
Normal file
80
mealie/db/models/household/household.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..group import Group
|
||||
from ..users import User
|
||||
from . import (
|
||||
CookBook,
|
||||
GroupEventNotifierModel,
|
||||
GroupInviteToken,
|
||||
GroupRecipeAction,
|
||||
GroupWebhooksModel,
|
||||
HouseholdPreferencesModel,
|
||||
)
|
||||
|
||||
|
||||
class Household(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "households"
|
||||
__table_args__ = (
|
||||
sa.UniqueConstraint("group_id", "name", name="household_name_group_id_key"),
|
||||
sa.UniqueConstraint("group_id", "slug", name="household_slug_group_id_key"),
|
||||
)
|
||||
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
name: Mapped[str] = mapped_column(sa.String, index=True, nullable=False)
|
||||
slug: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
|
||||
invite_tokens: Mapped[list["GroupInviteToken"]] = orm.relationship(
|
||||
"GroupInviteToken", back_populates="household", cascade="all, delete-orphan"
|
||||
)
|
||||
preferences: Mapped["HouseholdPreferencesModel"] = orm.relationship(
|
||||
"HouseholdPreferencesModel",
|
||||
back_populates="household",
|
||||
uselist=False,
|
||||
single_parent=True,
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="households")
|
||||
users: Mapped[list["User"]] = orm.relationship("User", back_populates="household")
|
||||
|
||||
COMMON_ARGS = {
|
||||
"back_populates": "household",
|
||||
"cascade": "all, delete-orphan",
|
||||
"single_parent": True,
|
||||
}
|
||||
|
||||
recipe_actions: Mapped[list["GroupRecipeAction"]] = orm.relationship("GroupRecipeAction", **COMMON_ARGS)
|
||||
cookbooks: Mapped[list["CookBook"]] = orm.relationship("CookBook", **COMMON_ARGS)
|
||||
|
||||
webhooks: Mapped[list["GroupWebhooksModel"]] = orm.relationship("GroupWebhooksModel", **COMMON_ARGS)
|
||||
group_event_notifiers: Mapped[list["GroupEventNotifierModel"]] = orm.relationship(
|
||||
"GroupEventNotifierModel", **COMMON_ARGS
|
||||
)
|
||||
|
||||
model_config = ConfigDict(
|
||||
exclude={
|
||||
"users",
|
||||
"webhooks",
|
||||
"recipe_actions",
|
||||
"cookbooks",
|
||||
"preferences",
|
||||
"invite_tokens",
|
||||
"group_event_notifiers",
|
||||
"group",
|
||||
}
|
||||
)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
||||
@@ -8,7 +8,8 @@ from .._model_utils import guid
|
||||
from .._model_utils.auto_init import auto_init
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .group import Group
|
||||
from ..group import Group
|
||||
from .household import Household
|
||||
|
||||
|
||||
class GroupInviteToken(SqlAlchemyBase, BaseMixins):
|
||||
@@ -18,6 +19,8 @@ class GroupInviteToken(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
group_id: Mapped[guid.GUID | None] = mapped_column(guid.GUID, ForeignKey("groups.id"), index=True)
|
||||
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="invite_tokens")
|
||||
household_id: Mapped[guid.GUID | None] = mapped_column(guid.GUID, ForeignKey("households.id"), index=True)
|
||||
household: Mapped[Optional["Household"]] = orm.relationship("Household", back_populates="invite_tokens")
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_):
|
||||
@@ -1,7 +1,8 @@
|
||||
from datetime import date
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from sqlalchemy import Date, ForeignKey, String, orm
|
||||
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from mealie.db.models.recipe.tag import Tag, plan_rules_to_tags
|
||||
@@ -12,9 +13,10 @@ from .._model_utils.guid import GUID
|
||||
from ..recipe.category import Category, plan_rules_to_categories
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..group import Group
|
||||
from ..recipe import RecipeModel
|
||||
from ..users import User
|
||||
from .group import Group
|
||||
from .household import Household
|
||||
|
||||
|
||||
class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
|
||||
@@ -22,6 +24,7 @@ class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
|
||||
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
household_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("households.id"), index=True)
|
||||
|
||||
day: Mapped[str] = mapped_column(
|
||||
String, nullable=False, default="unset"
|
||||
@@ -41,13 +44,15 @@ class GroupMealPlanRules(BaseMixins, SqlAlchemyBase):
|
||||
class GroupMealPlan(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "group_meal_plans"
|
||||
|
||||
date: Mapped[date] = mapped_column(Date, index=True, nullable=False)
|
||||
date: Mapped[datetime.date] = mapped_column(Date, index=True, nullable=False)
|
||||
entry_type: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||
title: Mapped[str] = mapped_column(String, index=True, nullable=False)
|
||||
text: Mapped[str] = mapped_column(String, nullable=False)
|
||||
|
||||
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), index=True)
|
||||
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="mealplans")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("user", "household_id")
|
||||
household: AssociationProxy["Household"] = association_proxy("user", "household")
|
||||
user_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("users.id"), index=True)
|
||||
user: Mapped[Optional["User"]] = orm.relationship("User", back_populates="mealplans")
|
||||
|
||||
37
mealie/db/models/household/preferences.py
Normal file
37
mealie/db/models/household/preferences.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .household import Household
|
||||
|
||||
|
||||
class HouseholdPreferencesModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "household_preferences"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
household_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("households.id"), nullable=False, index=True)
|
||||
household: Mapped[Optional["Household"]] = orm.relationship("Household", back_populates="preferences")
|
||||
group_id: AssociationProxy[GUID] = association_proxy("household", "group_id")
|
||||
|
||||
private_household: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||
first_day_of_week: Mapped[int | None] = mapped_column(sa.Integer, default=0)
|
||||
|
||||
# Recipe Defaults
|
||||
recipe_public: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||
recipe_show_nutrition: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
recipe_show_assets: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
recipe_landscape_view: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
recipe_disable_comments: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
recipe_disable_amount: Mapped[bool | None] = mapped_column(sa.Boolean, default=True)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
||||
@@ -8,7 +8,8 @@ from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .group import Group
|
||||
from ..group import Group
|
||||
from .household import Household
|
||||
|
||||
|
||||
class GroupRecipeAction(SqlAlchemyBase, BaseMixins):
|
||||
@@ -16,6 +17,8 @@ class GroupRecipeAction(SqlAlchemyBase, BaseMixins):
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), index=True)
|
||||
group: Mapped["Group"] = relationship("Group", back_populates="recipe_actions", single_parent=True)
|
||||
household_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("households.id"), index=True)
|
||||
household: Mapped["Household"] = relationship("Household", back_populates="recipe_actions")
|
||||
|
||||
action_type: Mapped[str] = mapped_column(String, index=True)
|
||||
title: Mapped[str] = mapped_column(String, index=True)
|
||||
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import Boolean, Float, ForeignKey, Integer, String, UniqueConstraint, event, orm
|
||||
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
@@ -16,22 +17,30 @@ from .._model_utils.guid import GUID
|
||||
from ..recipe.ingredient import IngredientFoodModel, IngredientUnitModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..group import Group
|
||||
from ..recipe import RecipeModel
|
||||
from ..users import User
|
||||
from .group import Group
|
||||
from .household import Household
|
||||
|
||||
|
||||
class ShoppingListItemRecipeReference(BaseMixins, SqlAlchemyBase):
|
||||
__tablename__ = "shopping_list_item_recipe_reference"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
shopping_list_item: Mapped["ShoppingListItem"] = orm.relationship(
|
||||
"ShoppingListItem", back_populates="recipe_references"
|
||||
)
|
||||
shopping_list_item_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("shopping_list_items.id"), primary_key=True)
|
||||
|
||||
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
|
||||
recipe: Mapped[Optional["RecipeModel"]] = orm.relationship("RecipeModel", back_populates="shopping_list_item_refs")
|
||||
recipe_quantity: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
recipe_scale: Mapped[float] = mapped_column(Float, default=1)
|
||||
recipe_note: Mapped[str | None] = mapped_column(String)
|
||||
|
||||
group_id: AssociationProxy[GUID] = association_proxy("shopping_list_item", "group_id")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("shopping_list_item", "household_id")
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
||||
@@ -42,8 +51,12 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
# Id's
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
shopping_list: Mapped["ShoppingList"] = orm.relationship("ShoppingList", back_populates="list_items")
|
||||
shopping_list_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("shopping_lists.id"), index=True)
|
||||
|
||||
group_id: AssociationProxy[GUID] = association_proxy("shopping_list", "group_id")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("shopping_list", "household_id")
|
||||
|
||||
# Meta
|
||||
is_ingredient: Mapped[bool | None] = mapped_column(Boolean, default=True)
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0, index=True)
|
||||
@@ -85,7 +98,10 @@ class ShoppingListRecipeReference(BaseMixins, SqlAlchemyBase):
|
||||
__tablename__ = "shopping_list_recipe_reference"
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
shopping_list: Mapped["ShoppingList"] = orm.relationship("ShoppingList", back_populates="recipe_references")
|
||||
shopping_list_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("shopping_lists.id"), primary_key=True)
|
||||
group_id: AssociationProxy[GUID] = association_proxy("shopping_list", "group_id")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("shopping_list", "household_id")
|
||||
|
||||
recipe_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("recipes.id"), index=True)
|
||||
recipe: Mapped[Optional["RecipeModel"]] = orm.relationship(
|
||||
@@ -107,10 +123,15 @@ class ShoppingListMultiPurposeLabel(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
shopping_list_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("shopping_lists.id"), primary_key=True)
|
||||
shopping_list: Mapped["ShoppingList"] = orm.relationship("ShoppingList", back_populates="label_settings")
|
||||
|
||||
label_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("multi_purpose_labels.id"), primary_key=True)
|
||||
label: Mapped["MultiPurposeLabel"] = orm.relationship(
|
||||
"MultiPurposeLabel", back_populates="shopping_lists_label_settings"
|
||||
)
|
||||
|
||||
group_id: AssociationProxy[GUID] = association_proxy("shopping_list", "group_id")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("shopping_list", "household_id")
|
||||
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
model_config = ConfigDict(exclude={"label"})
|
||||
|
||||
@@ -125,6 +146,8 @@ class ShoppingList(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="shopping_lists")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("user", "household_id")
|
||||
household: AssociationProxy["Household"] = association_proxy("user", "household")
|
||||
user_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("users.id"), nullable=False, index=True)
|
||||
user: Mapped["User"] = orm.relationship("User", back_populates="shopping_lists")
|
||||
|
||||
@@ -178,7 +201,7 @@ session_buffer_context = ContextVar("session_buffer", default=SessionBuffer())
|
||||
@event.listens_for(ShoppingListItem, "after_update")
|
||||
@event.listens_for(ShoppingListItem, "after_delete")
|
||||
def buffer_shopping_list_updates(_, connection, target: ShoppingListItem):
|
||||
"""Adds the shopping list id to the session buffer so its `update_at` property can be updated later"""
|
||||
"""Adds the shopping list id to the session buffer so its `updated_at` property can be updated later"""
|
||||
|
||||
session_buffer = session_buffer_context.get()
|
||||
session_buffer.add(target.shopping_list_id)
|
||||
@@ -186,7 +209,7 @@ def buffer_shopping_list_updates(_, connection, target: ShoppingListItem):
|
||||
|
||||
@event.listens_for(orm.Session, "after_flush")
|
||||
def update_shopping_lists(session: orm.Session, _):
|
||||
"""Pulls all pending shopping list updates from the buffer and updates their `update_at` property"""
|
||||
"""Pulls all pending shopping list updates from the buffer and updates their `updated_at` property"""
|
||||
|
||||
session_buffer = session_buffer_context.get()
|
||||
if not session_buffer.shopping_list_ids:
|
||||
@@ -204,7 +227,7 @@ def update_shopping_lists(session: orm.Session, _):
|
||||
if not shopping_list:
|
||||
continue
|
||||
|
||||
shopping_list.update_at = datetime.now(timezone.utc)
|
||||
shopping_list.updated_at = datetime.now(timezone.utc)
|
||||
local_session.commit()
|
||||
except Exception:
|
||||
local_session.rollback()
|
||||
@@ -9,7 +9,8 @@ from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .group import Group
|
||||
from ..group import Group
|
||||
from .household import Household
|
||||
|
||||
|
||||
class GroupWebhooksModel(SqlAlchemyBase, BaseMixins):
|
||||
@@ -18,6 +19,10 @@ class GroupWebhooksModel(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
group: Mapped[Optional["Group"]] = orm.relationship("Group", back_populates="webhooks", single_parent=True)
|
||||
group_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("groups.id"), index=True)
|
||||
household: Mapped[Optional["Household"]] = orm.relationship(
|
||||
"Household", back_populates="webhooks", single_parent=True
|
||||
)
|
||||
household_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("households.id"), index=True)
|
||||
|
||||
enabled: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||
name: Mapped[str | None] = mapped_column(String)
|
||||
@@ -27,7 +32,7 @@ class GroupWebhooksModel(SqlAlchemyBase, BaseMixins):
|
||||
webhook_type: Mapped[str | None] = mapped_column(String, default="") # Future use for different types of webhooks
|
||||
scheduled_time: Mapped[time | None] = mapped_column(Time, default=lambda: datetime.now(timezone.utc).time())
|
||||
|
||||
# Columne is no longer used but is kept for since it's super annoying to
|
||||
# Column is no longer used but is kept for since it's super annoying to
|
||||
# delete a column in SQLite and it's not a big deal to keep it around
|
||||
time: Mapped[str | None] = mapped_column(String, default="00:00")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String, orm
|
||||
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
@@ -21,6 +22,9 @@ class RecipeComment(SqlAlchemyBase, BaseMixins):
|
||||
recipe_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("recipes.id"), nullable=False, index=True)
|
||||
recipe: Mapped["RecipeModel"] = orm.relationship("RecipeModel", back_populates="comments")
|
||||
|
||||
group_id: AssociationProxy[GUID] = association_proxy("recipe", "group_id")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("recipe", "household_id")
|
||||
|
||||
# User Link
|
||||
user_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("users.id"), nullable=False, index=True)
|
||||
user: Mapped["User"] = orm.relationship(
|
||||
|
||||
@@ -5,6 +5,7 @@ import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
from sqlalchemy.orm import Mapped, mapped_column, validates
|
||||
from sqlalchemy.orm.attributes import get_history
|
||||
@@ -31,7 +32,8 @@ from .tag import recipes_to_tags
|
||||
from .tool import recipes_to_tools
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..group import Group, GroupMealPlan, ShoppingListItemRecipeReference, ShoppingListRecipeReference
|
||||
from ..group import Group, GroupMealPlan
|
||||
from ..household import Household, ShoppingListItemRecipeReference, ShoppingListRecipeReference
|
||||
from ..users import User
|
||||
from . import Category, Tag, Tool
|
||||
|
||||
@@ -49,19 +51,25 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="recipes", foreign_keys=[group_id])
|
||||
|
||||
household_id: AssociationProxy[GUID] = association_proxy("user", "household_id")
|
||||
household: AssociationProxy["Household"] = association_proxy("user", "household")
|
||||
|
||||
user_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("users.id", use_alter=True), index=True)
|
||||
user: Mapped["User"] = orm.relationship("User", uselist=False, foreign_keys=[user_id])
|
||||
|
||||
rating: Mapped[float | None] = mapped_column(sa.Float, index=True, nullable=True)
|
||||
rated_by: Mapped[list["User"]] = orm.relationship(
|
||||
"User", secondary=UserToRecipe.__tablename__, back_populates="rated_recipes"
|
||||
"User",
|
||||
secondary=UserToRecipe.__tablename__,
|
||||
back_populates="rated_recipes",
|
||||
overlaps="recipe,favorited_by,favorited_recipes",
|
||||
)
|
||||
favorited_by: Mapped[list["User"]] = orm.relationship(
|
||||
"User",
|
||||
secondary=UserToRecipe.__tablename__,
|
||||
primaryjoin="and_(RecipeModel.id==UserToRecipe.recipe_id, UserToRecipe.is_favorite==True)",
|
||||
back_populates="favorite_recipes",
|
||||
viewonly=True,
|
||||
overlaps="recipe,rated_by,rated_recipes",
|
||||
)
|
||||
|
||||
meal_entries: Mapped[list["GroupMealPlan"]] = orm.relationship(
|
||||
|
||||
@@ -2,6 +2,7 @@ from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String
|
||||
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
@@ -21,6 +22,9 @@ class RecipeTimelineEvent(SqlAlchemyBase, BaseMixins):
|
||||
recipe_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("recipes.id"), nullable=False, index=True)
|
||||
recipe: Mapped["RecipeModel"] = relationship("RecipeModel", back_populates="timeline_events")
|
||||
|
||||
group_id: AssociationProxy[GUID] = association_proxy("recipe", "group_id")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("recipe", "household_id")
|
||||
|
||||
# Related User (Actor)
|
||||
user_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("users.id"), nullable=False, index=True)
|
||||
user: Mapped["User"] = relationship(
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, Column, Float, ForeignKey, UniqueConstraint, event
|
||||
from sqlalchemy.engine.base import Connection
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils.auto_init import auto_init
|
||||
from .._model_utils.guid import GUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..recipe import RecipeModel
|
||||
|
||||
|
||||
class UserToRecipe(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "users_to_recipes"
|
||||
@@ -14,7 +20,11 @@ class UserToRecipe(SqlAlchemyBase, BaseMixins):
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
user_id = Column(GUID, ForeignKey("users.id"), index=True, primary_key=True)
|
||||
recipe: Mapped["RecipeModel"] = relationship("RecipeModel")
|
||||
recipe_id = Column(GUID, ForeignKey("recipes.id"), index=True, primary_key=True)
|
||||
group_id: AssociationProxy[GUID] = association_proxy("recipe", "group_id")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("recipe", "household_id")
|
||||
|
||||
rating = Column(Float, index=True, nullable=True)
|
||||
is_favorite = Column(Boolean, index=True, nullable=False)
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Integer, String, orm
|
||||
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Integer, String, orm, select
|
||||
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from mealie.core.config import get_app_settings
|
||||
from mealie.db.models._model_utils.auto_init import auto_init
|
||||
@@ -16,8 +17,9 @@ from .user_to_recipe import UserToRecipe
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..group import Group
|
||||
from ..group.mealplan import GroupMealPlan
|
||||
from ..group.shopping_list import ShoppingList
|
||||
from ..household import Household
|
||||
from ..household.mealplan import GroupMealPlan
|
||||
from ..household.shopping_list import ShoppingList
|
||||
from ..recipe import RecipeComment, RecipeModel, RecipeTimelineEvent
|
||||
from .password_reset import PasswordResetModel
|
||||
|
||||
@@ -30,6 +32,9 @@ class LongLiveToken(SqlAlchemyBase, BaseMixins):
|
||||
user_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("users.id"), index=True)
|
||||
user: Mapped[Optional["User"]] = orm.relationship("User")
|
||||
|
||||
group_id: AssociationProxy[GUID] = association_proxy("user", "group_id")
|
||||
household_id: AssociationProxy[GUID] = association_proxy("user", "household_id")
|
||||
|
||||
def __init__(self, name, token, user_id, **_) -> None:
|
||||
self.name = name
|
||||
self.token = token
|
||||
@@ -55,6 +60,8 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="users")
|
||||
household_id: Mapped[GUID | None] = mapped_column(GUID, ForeignKey("households.id"), nullable=True, index=True)
|
||||
household: Mapped["Household"] = orm.relationship("Household", back_populates="users")
|
||||
|
||||
cache_key: Mapped[str | None] = mapped_column(String, default="1234")
|
||||
login_attemps: Mapped[int | None] = mapped_column(Integer, default=0)
|
||||
@@ -85,14 +92,17 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
)
|
||||
shopping_lists: Mapped[Optional["ShoppingList"]] = orm.relationship("ShoppingList", **sp_args)
|
||||
rated_recipes: Mapped[list["RecipeModel"]] = orm.relationship(
|
||||
"RecipeModel", secondary=UserToRecipe.__tablename__, back_populates="rated_by"
|
||||
"RecipeModel",
|
||||
secondary=UserToRecipe.__tablename__,
|
||||
back_populates="rated_by",
|
||||
overlaps="recipe,favorited_by,favorited_recipes",
|
||||
)
|
||||
favorite_recipes: Mapped[list["RecipeModel"]] = orm.relationship(
|
||||
"RecipeModel",
|
||||
secondary=UserToRecipe.__tablename__,
|
||||
primaryjoin="and_(User.id==UserToRecipe.user_id, UserToRecipe.is_favorite==True)",
|
||||
back_populates="favorited_by",
|
||||
viewonly=True,
|
||||
overlaps="recipe,rated_by,rated_recipes",
|
||||
)
|
||||
model_config = ConfigDict(
|
||||
exclude={
|
||||
@@ -102,6 +112,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
"can_invite",
|
||||
"can_organize",
|
||||
"group",
|
||||
"household",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -109,15 +120,33 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
def group_slug(self) -> str:
|
||||
return self.group.slug
|
||||
|
||||
@hybrid_property
|
||||
def household_slug(self) -> str:
|
||||
return self.household.slug
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, session, full_name, password, group: str | None = None, **kwargs) -> None:
|
||||
if group is None:
|
||||
def __init__(
|
||||
self, session: Session, full_name, password, group: str | None = None, household: str | None = None, **kwargs
|
||||
) -> None:
|
||||
if group is None or household is None:
|
||||
settings = get_app_settings()
|
||||
group = settings.DEFAULT_GROUP
|
||||
group = group or settings.DEFAULT_GROUP
|
||||
household = household or settings.DEFAULT_HOUSEHOLD
|
||||
|
||||
from mealie.db.models.group import Group
|
||||
from mealie.db.models.household import Household
|
||||
|
||||
self.group = Group.get_by_name(session, group)
|
||||
self.group = session.execute(select(Group).filter(Group.name == group)).scalars().one_or_none()
|
||||
if self.group:
|
||||
self.household = (
|
||||
session.execute(
|
||||
select(Household).filter(Household.name == household, Household.group_id == self.group.id)
|
||||
)
|
||||
.scalars()
|
||||
.one_or_none()
|
||||
)
|
||||
else:
|
||||
self.household = None
|
||||
|
||||
self.rated_recipes = []
|
||||
|
||||
@@ -129,14 +158,25 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
self._set_permissions(**kwargs)
|
||||
|
||||
@auto_init()
|
||||
def update(self, full_name, email, group, username, session=None, **kwargs):
|
||||
def update(self, session: Session, full_name, email, group, household, username, **kwargs):
|
||||
self.username = username
|
||||
self.full_name = full_name
|
||||
self.email = email
|
||||
|
||||
from mealie.db.models.group import Group
|
||||
from mealie.db.models.household import Household
|
||||
|
||||
self.group = Group.get_by_name(session, group)
|
||||
self.group = session.execute(select(Group).filter(Group.name == group)).scalars().one_or_none()
|
||||
if self.group:
|
||||
self.household = (
|
||||
session.execute(
|
||||
select(Household).filter(Household.name == household, Household.group_id == self.group.id)
|
||||
)
|
||||
.scalars()
|
||||
.one_or_none()
|
||||
)
|
||||
else:
|
||||
self.household = None
|
||||
|
||||
if self.username is None:
|
||||
self.username = full_name
|
||||
|
||||
@@ -34,7 +34,7 @@ class AsyncSafeTransport(httpx.AsyncBaseTransport):
|
||||
self._wrapper = httpx.AsyncHTTPTransport(**kwargs)
|
||||
self._log = log
|
||||
|
||||
async def handle_async_request(self, request):
|
||||
async def handle_async_request(self, request) -> httpx.Response:
|
||||
# override timeout value for _all_ requests
|
||||
request.extensions["timeout"] = httpx.Timeout(self.timeout, pool=self.timeout).as_dict()
|
||||
|
||||
|
||||
6
mealie/repos/_utils.py
Normal file
6
mealie/repos/_utils.py
Normal file
@@ -0,0 +1,6 @@
|
||||
class NotSet:
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
|
||||
NOT_SET = NotSet()
|
||||
@@ -1,7 +1,11 @@
|
||||
from pydantic import UUID4
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ._utils import NOT_SET, NotSet
|
||||
from .repository_factory import AllRepositories
|
||||
|
||||
|
||||
def get_repositories(session: Session):
|
||||
return AllRepositories(session)
|
||||
def get_repositories(
|
||||
session: Session, *, group_id: UUID4 | None | NotSet = NOT_SET, household_id: UUID4 | None | NotSet = NOT_SET
|
||||
):
|
||||
return AllRepositories(session, group_id=group_id, household_id=household_id)
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
from collections.abc import Sequence
|
||||
from functools import cached_property
|
||||
|
||||
from pydantic import UUID4
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from mealie.db.models.group import Group, GroupMealPlan, ReportEntryModel, ReportModel
|
||||
from mealie.db.models.group.cookbook import CookBook
|
||||
from mealie.db.models.group.events import GroupEventNotifierModel
|
||||
from mealie.db.models.group import Group, ReportEntryModel, ReportModel
|
||||
from mealie.db.models.group.exports import GroupDataExportsModel
|
||||
from mealie.db.models.group.invite_tokens import GroupInviteToken
|
||||
from mealie.db.models.group.mealplan import GroupMealPlanRules
|
||||
from mealie.db.models.group.preferences import GroupPreferencesModel
|
||||
from mealie.db.models.group.recipe_action import GroupRecipeAction
|
||||
from mealie.db.models.group.shopping_list import (
|
||||
from mealie.db.models.household.cookbook import CookBook
|
||||
from mealie.db.models.household.events import GroupEventNotifierModel
|
||||
from mealie.db.models.household.household import Household
|
||||
from mealie.db.models.household.invite_tokens import GroupInviteToken
|
||||
from mealie.db.models.household.mealplan import GroupMealPlan, GroupMealPlanRules
|
||||
from mealie.db.models.household.preferences import HouseholdPreferencesModel
|
||||
from mealie.db.models.household.recipe_action import GroupRecipeAction
|
||||
from mealie.db.models.household.shopping_list import (
|
||||
ShoppingList,
|
||||
ShoppingListItem,
|
||||
ShoppingListItemRecipeReference,
|
||||
ShoppingListMultiPurposeLabel,
|
||||
ShoppingListRecipeReference,
|
||||
)
|
||||
from mealie.db.models.group.webhooks import GroupWebhooksModel
|
||||
from mealie.db.models.household.webhooks import GroupWebhooksModel
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.category import Category
|
||||
from mealie.db.models.recipe.comment import RecipeComment
|
||||
@@ -33,22 +36,25 @@ from mealie.db.models.users import LongLiveToken, User
|
||||
from mealie.db.models.users.password_reset import PasswordResetModel
|
||||
from mealie.db.models.users.user_to_recipe import UserToRecipe
|
||||
from mealie.repos.repository_foods import RepositoryFood
|
||||
from mealie.repos.repository_household import RepositoryHousehold
|
||||
from mealie.repos.repository_meal_plan_rules import RepositoryMealPlanRules
|
||||
from mealie.repos.repository_units import RepositoryUnit
|
||||
from mealie.schema.cookbook.cookbook import ReadCookBook
|
||||
from mealie.schema.group.group_events import GroupEventNotifierOut
|
||||
from mealie.schema.group.group_exports import GroupDataExport
|
||||
from mealie.schema.group.group_preferences import ReadGroupPreferences
|
||||
from mealie.schema.group.group_recipe_action import GroupRecipeActionOut
|
||||
from mealie.schema.group.group_shopping_list import (
|
||||
from mealie.schema.household.group_events import GroupEventNotifierOut
|
||||
from mealie.schema.household.group_recipe_action import GroupRecipeActionOut
|
||||
from mealie.schema.household.group_shopping_list import (
|
||||
ShoppingListItemOut,
|
||||
ShoppingListItemRecipeRefOut,
|
||||
ShoppingListMultiPurposeLabelOut,
|
||||
ShoppingListOut,
|
||||
ShoppingListRecipeRefOut,
|
||||
)
|
||||
from mealie.schema.group.invite_token import ReadInviteToken
|
||||
from mealie.schema.group.webhook import ReadWebhook
|
||||
from mealie.schema.household.household import HouseholdInDB
|
||||
from mealie.schema.household.household_preferences import ReadHouseholdPreferences
|
||||
from mealie.schema.household.invite_token import ReadInviteToken
|
||||
from mealie.schema.household.webhook import ReadWebhook
|
||||
from mealie.schema.labels import MultiPurposeLabelOut
|
||||
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
|
||||
from mealie.schema.meal_plan.plan_rules import PlanRulesOut
|
||||
@@ -62,7 +68,8 @@ from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser
|
||||
from mealie.schema.user.user import UserRatingOut
|
||||
from mealie.schema.user.user_passwords import PrivatePasswordResetToken
|
||||
|
||||
from .repository_generic import RepositoryGeneric
|
||||
from ._utils import NOT_SET, NotSet
|
||||
from .repository_generic import GroupRepositoryGeneric, HouseholdRepositoryGeneric
|
||||
from .repository_group import RepositoryGroup
|
||||
from .repository_meals import RepositoryMeals
|
||||
from .repository_recipes import RepositoryRecipes
|
||||
@@ -73,89 +80,110 @@ PK_ID = "id"
|
||||
PK_SLUG = "slug"
|
||||
PK_TOKEN = "token"
|
||||
PK_GROUP_ID = "group_id"
|
||||
PK_HOUSEHOLD_ID = "household_id"
|
||||
|
||||
|
||||
class RepositoryCategories(RepositoryGeneric[CategoryOut, Category]):
|
||||
class RepositoryCategories(GroupRepositoryGeneric[CategoryOut, Category]):
|
||||
def get_empty(self) -> Sequence[Category]:
|
||||
stmt = select(Category).filter(~Category.recipes.any())
|
||||
|
||||
return self.session.execute(stmt).scalars().all()
|
||||
|
||||
|
||||
class RepositoryTags(RepositoryGeneric[TagOut, Tag]):
|
||||
class RepositoryTags(GroupRepositoryGeneric[TagOut, Tag]):
|
||||
def get_empty(self) -> Sequence[Tag]:
|
||||
stmt = select(Tag).filter(~Tag.recipes.any())
|
||||
return self.session.execute(stmt).scalars().all()
|
||||
|
||||
|
||||
class AllRepositories:
|
||||
def __init__(self, session: Session) -> None:
|
||||
"""
|
||||
`AllRepositories` class is the data access layer for all database actions within
|
||||
Mealie. Database uses composition from classes derived from AccessModel. These
|
||||
can be substantiated from the AccessModel class or through inheritance when
|
||||
additional methods are required.
|
||||
"""
|
||||
"""
|
||||
`AllRepositories` class is the data access layer for all database actions within
|
||||
Mealie. Database uses composition from classes derived from AccessModel. These
|
||||
can be substantiated from the AccessModel class or through inheritance when
|
||||
additional methods are required.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: Session,
|
||||
*,
|
||||
group_id: UUID4 | None | NotSet = NOT_SET,
|
||||
household_id: UUID4 | None | NotSet = NOT_SET,
|
||||
) -> None:
|
||||
self.session = session
|
||||
self.group_id = group_id
|
||||
self.household_id = household_id
|
||||
|
||||
# ================================================================
|
||||
# Recipe
|
||||
|
||||
@cached_property
|
||||
def recipes(self) -> RepositoryRecipes:
|
||||
return RepositoryRecipes(self.session, PK_SLUG, RecipeModel, Recipe)
|
||||
return RepositoryRecipes(
|
||||
self.session, PK_SLUG, RecipeModel, Recipe, group_id=self.group_id, household_id=self.household_id
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def ingredient_foods(self) -> RepositoryFood:
|
||||
return RepositoryFood(self.session, PK_ID, IngredientFoodModel, IngredientFood)
|
||||
return RepositoryFood(self.session, PK_ID, IngredientFoodModel, IngredientFood, group_id=self.group_id)
|
||||
|
||||
@cached_property
|
||||
def ingredient_units(self) -> RepositoryUnit:
|
||||
return RepositoryUnit(self.session, PK_ID, IngredientUnitModel, IngredientUnit)
|
||||
return RepositoryUnit(self.session, PK_ID, IngredientUnitModel, IngredientUnit, group_id=self.group_id)
|
||||
|
||||
@cached_property
|
||||
def tools(self) -> RepositoryGeneric[RecipeToolOut, Tool]:
|
||||
return RepositoryGeneric(self.session, PK_ID, Tool, RecipeToolOut)
|
||||
def tools(self) -> GroupRepositoryGeneric[RecipeToolOut, Tool]:
|
||||
return GroupRepositoryGeneric(self.session, PK_ID, Tool, RecipeToolOut, group_id=self.group_id)
|
||||
|
||||
@cached_property
|
||||
def comments(self) -> RepositoryGeneric[RecipeCommentOut, RecipeComment]:
|
||||
return RepositoryGeneric(self.session, PK_ID, RecipeComment, RecipeCommentOut)
|
||||
def comments(self) -> GroupRepositoryGeneric[RecipeCommentOut, RecipeComment]:
|
||||
# Since users can comment on recipes that belong to other households,
|
||||
# this is a group repository, rather than a household repository.
|
||||
return GroupRepositoryGeneric(self.session, PK_ID, RecipeComment, RecipeCommentOut, group_id=self.group_id)
|
||||
|
||||
@cached_property
|
||||
def categories(self) -> RepositoryCategories:
|
||||
return RepositoryCategories(self.session, PK_ID, Category, CategoryOut)
|
||||
return RepositoryCategories(self.session, PK_ID, Category, CategoryOut, group_id=self.group_id)
|
||||
|
||||
@cached_property
|
||||
def tags(self) -> RepositoryTags:
|
||||
return RepositoryTags(self.session, PK_ID, Tag, TagOut)
|
||||
return RepositoryTags(self.session, PK_ID, Tag, TagOut, group_id=self.group_id)
|
||||
|
||||
@cached_property
|
||||
def recipe_share_tokens(self) -> RepositoryGeneric[RecipeShareToken, RecipeShareTokenModel]:
|
||||
return RepositoryGeneric(self.session, PK_ID, RecipeShareTokenModel, RecipeShareToken)
|
||||
def recipe_share_tokens(self) -> GroupRepositoryGeneric[RecipeShareToken, RecipeShareTokenModel]:
|
||||
return GroupRepositoryGeneric(
|
||||
self.session, PK_ID, RecipeShareTokenModel, RecipeShareToken, group_id=self.group_id
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def recipe_timeline_events(self) -> RepositoryGeneric[RecipeTimelineEventOut, RecipeTimelineEvent]:
|
||||
return RepositoryGeneric(self.session, PK_ID, RecipeTimelineEvent, RecipeTimelineEventOut)
|
||||
def recipe_timeline_events(self) -> GroupRepositoryGeneric[RecipeTimelineEventOut, RecipeTimelineEvent]:
|
||||
# Since users can post events on recipes that belong to other households,
|
||||
# this is a group repository, rather than a household repository.
|
||||
return GroupRepositoryGeneric(
|
||||
self.session, PK_ID, RecipeTimelineEvent, RecipeTimelineEventOut, group_id=self.group_id
|
||||
)
|
||||
|
||||
# ================================================================
|
||||
# User
|
||||
|
||||
@cached_property
|
||||
def users(self) -> RepositoryUsers:
|
||||
return RepositoryUsers(self.session, PK_ID, User, PrivateUser)
|
||||
return RepositoryUsers(self.session, PK_ID, User, PrivateUser, group_id=self.group_id)
|
||||
|
||||
@cached_property
|
||||
def user_ratings(self) -> RepositoryUserRatings:
|
||||
return RepositoryUserRatings(self.session, PK_ID, UserToRecipe, UserRatingOut)
|
||||
return RepositoryUserRatings(self.session, PK_ID, UserToRecipe, UserRatingOut, group_id=self.group_id)
|
||||
|
||||
@cached_property
|
||||
def api_tokens(self) -> RepositoryGeneric[LongLiveTokenInDB, LongLiveToken]:
|
||||
return RepositoryGeneric(self.session, PK_ID, LongLiveToken, LongLiveTokenInDB)
|
||||
def api_tokens(self) -> GroupRepositoryGeneric[LongLiveTokenInDB, LongLiveToken]:
|
||||
return GroupRepositoryGeneric(self.session, PK_ID, LongLiveToken, LongLiveTokenInDB, group_id=self.group_id)
|
||||
|
||||
@cached_property
|
||||
def tokens_pw_reset(self) -> RepositoryGeneric[PrivatePasswordResetToken, PasswordResetModel]:
|
||||
return RepositoryGeneric(self.session, PK_TOKEN, PasswordResetModel, PrivatePasswordResetToken)
|
||||
def tokens_pw_reset(self) -> GroupRepositoryGeneric[PrivatePasswordResetToken, PasswordResetModel]:
|
||||
return GroupRepositoryGeneric(
|
||||
self.session, PK_TOKEN, PasswordResetModel, PrivatePasswordResetToken, group_id=self.group_id
|
||||
)
|
||||
|
||||
# ================================================================
|
||||
# Group
|
||||
@@ -165,84 +193,172 @@ class AllRepositories:
|
||||
return RepositoryGroup(self.session, PK_ID, Group, GroupInDB)
|
||||
|
||||
@cached_property
|
||||
def group_invite_tokens(self) -> RepositoryGeneric[ReadInviteToken, GroupInviteToken]:
|
||||
return RepositoryGeneric(self.session, PK_TOKEN, GroupInviteToken, ReadInviteToken)
|
||||
def group_preferences(self) -> GroupRepositoryGeneric[ReadGroupPreferences, GroupPreferencesModel]:
|
||||
return GroupRepositoryGeneric(
|
||||
self.session, PK_GROUP_ID, GroupPreferencesModel, ReadGroupPreferences, group_id=self.group_id
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def group_preferences(self) -> RepositoryGeneric[ReadGroupPreferences, GroupPreferencesModel]:
|
||||
return RepositoryGeneric(self.session, PK_GROUP_ID, GroupPreferencesModel, ReadGroupPreferences)
|
||||
def group_exports(self) -> GroupRepositoryGeneric[GroupDataExport, GroupDataExportsModel]:
|
||||
return GroupRepositoryGeneric(
|
||||
self.session, PK_ID, GroupDataExportsModel, GroupDataExport, group_id=self.group_id
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def group_exports(self) -> RepositoryGeneric[GroupDataExport, GroupDataExportsModel]:
|
||||
return RepositoryGeneric(self.session, PK_ID, GroupDataExportsModel, GroupDataExport)
|
||||
def group_reports(self) -> GroupRepositoryGeneric[ReportOut, ReportModel]:
|
||||
return GroupRepositoryGeneric(self.session, PK_ID, ReportModel, ReportOut, group_id=self.group_id)
|
||||
|
||||
@cached_property
|
||||
def group_reports(self) -> RepositoryGeneric[ReportOut, ReportModel]:
|
||||
return RepositoryGeneric(self.session, PK_ID, ReportModel, ReportOut)
|
||||
def group_report_entries(self) -> GroupRepositoryGeneric[ReportEntryOut, ReportEntryModel]:
|
||||
return GroupRepositoryGeneric(self.session, PK_ID, ReportEntryModel, ReportEntryOut, group_id=self.group_id)
|
||||
|
||||
# ================================================================
|
||||
# Household
|
||||
|
||||
@cached_property
|
||||
def group_report_entries(self) -> RepositoryGeneric[ReportEntryOut, ReportEntryModel]:
|
||||
return RepositoryGeneric(self.session, PK_ID, ReportEntryModel, ReportEntryOut)
|
||||
def households(self) -> RepositoryHousehold:
|
||||
return RepositoryHousehold(self.session, PK_ID, Household, HouseholdInDB, group_id=self.group_id)
|
||||
|
||||
@cached_property
|
||||
def cookbooks(self) -> RepositoryGeneric[ReadCookBook, CookBook]:
|
||||
return RepositoryGeneric(self.session, PK_ID, CookBook, ReadCookBook)
|
||||
def household_preferences(self) -> HouseholdRepositoryGeneric[ReadHouseholdPreferences, HouseholdPreferencesModel]:
|
||||
return HouseholdRepositoryGeneric(
|
||||
self.session,
|
||||
PK_HOUSEHOLD_ID,
|
||||
HouseholdPreferencesModel,
|
||||
ReadHouseholdPreferences,
|
||||
group_id=self.group_id,
|
||||
household_id=self.household_id,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def group_recipe_actions(self) -> RepositoryGeneric[GroupRecipeActionOut, GroupRecipeAction]:
|
||||
return RepositoryGeneric(self.session, PK_ID, GroupRecipeAction, GroupRecipeActionOut)
|
||||
def cookbooks(self) -> HouseholdRepositoryGeneric[ReadCookBook, CookBook]:
|
||||
return HouseholdRepositoryGeneric(
|
||||
self.session, PK_ID, CookBook, ReadCookBook, group_id=self.group_id, household_id=self.household_id
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def group_invite_tokens(self) -> HouseholdRepositoryGeneric[ReadInviteToken, GroupInviteToken]:
|
||||
return HouseholdRepositoryGeneric(
|
||||
self.session,
|
||||
PK_TOKEN,
|
||||
GroupInviteToken,
|
||||
ReadInviteToken,
|
||||
group_id=self.group_id,
|
||||
household_id=self.household_id,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def group_recipe_actions(self) -> HouseholdRepositoryGeneric[GroupRecipeActionOut, GroupRecipeAction]:
|
||||
return HouseholdRepositoryGeneric(
|
||||
self.session,
|
||||
PK_ID,
|
||||
GroupRecipeAction,
|
||||
GroupRecipeActionOut,
|
||||
group_id=self.group_id,
|
||||
household_id=self.household_id,
|
||||
)
|
||||
|
||||
# ================================================================
|
||||
# Meal Plan
|
||||
|
||||
@cached_property
|
||||
def meals(self) -> RepositoryMeals:
|
||||
return RepositoryMeals(self.session, PK_ID, GroupMealPlan, ReadPlanEntry)
|
||||
return RepositoryMeals(
|
||||
self.session, PK_ID, GroupMealPlan, ReadPlanEntry, group_id=self.group_id, household_id=self.household_id
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def group_meal_plan_rules(self) -> RepositoryMealPlanRules:
|
||||
return RepositoryMealPlanRules(self.session, PK_ID, GroupMealPlanRules, PlanRulesOut)
|
||||
|
||||
@cached_property
|
||||
def webhooks(self) -> RepositoryGeneric[ReadWebhook, GroupWebhooksModel]:
|
||||
return RepositoryGeneric(self.session, PK_ID, GroupWebhooksModel, ReadWebhook)
|
||||
return RepositoryMealPlanRules(
|
||||
self.session,
|
||||
PK_ID,
|
||||
GroupMealPlanRules,
|
||||
PlanRulesOut,
|
||||
group_id=self.group_id,
|
||||
household_id=self.household_id,
|
||||
)
|
||||
|
||||
# ================================================================
|
||||
# Shopping List
|
||||
|
||||
@cached_property
|
||||
def group_shopping_lists(self) -> RepositoryShoppingList:
|
||||
return RepositoryShoppingList(self.session, PK_ID, ShoppingList, ShoppingListOut)
|
||||
return RepositoryShoppingList(
|
||||
self.session, PK_ID, ShoppingList, ShoppingListOut, group_id=self.group_id, household_id=self.household_id
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def group_shopping_list_item(self) -> RepositoryGeneric[ShoppingListItemOut, ShoppingListItem]:
|
||||
return RepositoryGeneric(self.session, PK_ID, ShoppingListItem, ShoppingListItemOut)
|
||||
def group_shopping_list_item(self) -> HouseholdRepositoryGeneric[ShoppingListItemOut, ShoppingListItem]:
|
||||
return HouseholdRepositoryGeneric(
|
||||
self.session,
|
||||
PK_ID,
|
||||
ShoppingListItem,
|
||||
ShoppingListItemOut,
|
||||
group_id=self.group_id,
|
||||
household_id=self.household_id,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def group_shopping_list_item_references(
|
||||
self,
|
||||
) -> RepositoryGeneric[ShoppingListItemRecipeRefOut, ShoppingListItemRecipeReference]:
|
||||
return RepositoryGeneric(self.session, PK_ID, ShoppingListItemRecipeReference, ShoppingListItemRecipeRefOut)
|
||||
) -> HouseholdRepositoryGeneric[ShoppingListItemRecipeRefOut, ShoppingListItemRecipeReference]:
|
||||
return HouseholdRepositoryGeneric(
|
||||
self.session,
|
||||
PK_ID,
|
||||
ShoppingListItemRecipeReference,
|
||||
ShoppingListItemRecipeRefOut,
|
||||
group_id=self.group_id,
|
||||
household_id=self.household_id,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def group_shopping_list_recipe_refs(
|
||||
self,
|
||||
) -> RepositoryGeneric[ShoppingListRecipeRefOut, ShoppingListRecipeReference]:
|
||||
return RepositoryGeneric(self.session, PK_ID, ShoppingListRecipeReference, ShoppingListRecipeRefOut)
|
||||
) -> HouseholdRepositoryGeneric[ShoppingListRecipeRefOut, ShoppingListRecipeReference]:
|
||||
return HouseholdRepositoryGeneric(
|
||||
self.session,
|
||||
PK_ID,
|
||||
ShoppingListRecipeReference,
|
||||
ShoppingListRecipeRefOut,
|
||||
group_id=self.group_id,
|
||||
household_id=self.household_id,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def shopping_list_multi_purpose_labels(
|
||||
self,
|
||||
) -> RepositoryGeneric[ShoppingListMultiPurposeLabelOut, ShoppingListMultiPurposeLabel]:
|
||||
return RepositoryGeneric(self.session, PK_ID, ShoppingListMultiPurposeLabel, ShoppingListMultiPurposeLabelOut)
|
||||
) -> HouseholdRepositoryGeneric[ShoppingListMultiPurposeLabelOut, ShoppingListMultiPurposeLabel]:
|
||||
return HouseholdRepositoryGeneric(
|
||||
self.session,
|
||||
PK_ID,
|
||||
ShoppingListMultiPurposeLabel,
|
||||
ShoppingListMultiPurposeLabelOut,
|
||||
group_id=self.group_id,
|
||||
household_id=self.household_id,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def group_multi_purpose_labels(self) -> RepositoryGeneric[MultiPurposeLabelOut, MultiPurposeLabel]:
|
||||
return RepositoryGeneric(self.session, PK_ID, MultiPurposeLabel, MultiPurposeLabelOut)
|
||||
def group_multi_purpose_labels(self) -> GroupRepositoryGeneric[MultiPurposeLabelOut, MultiPurposeLabel]:
|
||||
return GroupRepositoryGeneric(
|
||||
self.session, PK_ID, MultiPurposeLabel, MultiPurposeLabelOut, group_id=self.group_id
|
||||
)
|
||||
|
||||
# ================================================================
|
||||
# Group Events
|
||||
# Events
|
||||
|
||||
@cached_property
|
||||
def group_event_notifier(self) -> RepositoryGeneric[GroupEventNotifierOut, GroupEventNotifierModel]:
|
||||
return RepositoryGeneric(self.session, PK_ID, GroupEventNotifierModel, GroupEventNotifierOut)
|
||||
def group_event_notifier(self) -> HouseholdRepositoryGeneric[GroupEventNotifierOut, GroupEventNotifierModel]:
|
||||
return HouseholdRepositoryGeneric(
|
||||
self.session,
|
||||
PK_ID,
|
||||
GroupEventNotifierModel,
|
||||
GroupEventNotifierOut,
|
||||
group_id=self.group_id,
|
||||
household_id=self.household_id,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def webhooks(self) -> HouseholdRepositoryGeneric[ReadWebhook, GroupWebhooksModel]:
|
||||
return HouseholdRepositoryGeneric(
|
||||
self.session, PK_ID, GroupWebhooksModel, ReadWebhook, group_id=self.group_id, household_id=self.household_id
|
||||
)
|
||||
|
||||
@@ -4,10 +4,10 @@ from sqlalchemy import select
|
||||
from mealie.db.models.recipe.ingredient import IngredientFoodModel
|
||||
from mealie.schema.recipe.recipe_ingredient import IngredientFood
|
||||
|
||||
from .repository_generic import RepositoryGeneric
|
||||
from .repository_generic import GroupRepositoryGeneric
|
||||
|
||||
|
||||
class RepositoryFood(RepositoryGeneric[IngredientFood, IngredientFoodModel]):
|
||||
class RepositoryFood(GroupRepositoryGeneric[IngredientFood, IngredientFoodModel]):
|
||||
def _get_food(self, id: UUID4) -> IngredientFoodModel:
|
||||
stmt = select(self.model).filter_by(**self._filter_builder(**{"id": id}))
|
||||
return self.session.execute(stmt).scalars().one()
|
||||
@@ -26,6 +26,3 @@ class RepositoryFood(RepositoryGeneric[IngredientFood, IngredientFoodModel]):
|
||||
raise e
|
||||
|
||||
return self.get_one(to_food)
|
||||
|
||||
def by_group(self, group_id: UUID4) -> "RepositoryFood":
|
||||
return super().by_group(group_id)
|
||||
|
||||
@@ -19,6 +19,8 @@ from mealie.schema.response.pagination import OrderByNullPosition, OrderDirectio
|
||||
from mealie.schema.response.query_filter import QueryFilter
|
||||
from mealie.schema.response.query_search import SearchFilter
|
||||
|
||||
from ._utils import NOT_SET, NotSet
|
||||
|
||||
Schema = TypeVar("Schema", bound=MealieModel)
|
||||
Model = TypeVar("Model", bound=SqlAlchemyBase)
|
||||
|
||||
@@ -33,11 +35,18 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
Generic ([Model]): Represents the SqlAlchemyModel Model
|
||||
"""
|
||||
|
||||
user_id: UUID4 | None = None
|
||||
group_id: UUID4 | None = None
|
||||
session: Session
|
||||
|
||||
def __init__(self, session: Session, primary_key: str, sql_model: type[Model], schema: type[Schema]) -> None:
|
||||
_group_id: UUID4 | None = None
|
||||
_household_id: UUID4 | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: Session,
|
||||
primary_key: str,
|
||||
sql_model: type[Model],
|
||||
schema: type[Schema],
|
||||
) -> None:
|
||||
self.session = session
|
||||
self.primary_key = primary_key
|
||||
self.model = sql_model
|
||||
@@ -45,13 +54,13 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
|
||||
self.logger = get_logger()
|
||||
|
||||
def by_user(self: T, user_id: UUID4) -> T:
|
||||
self.user_id = user_id
|
||||
return self
|
||||
@property
|
||||
def group_id(self) -> UUID4 | None:
|
||||
return self._group_id
|
||||
|
||||
def by_group(self: T, group_id: UUID4) -> T:
|
||||
self.group_id = group_id
|
||||
return self
|
||||
@property
|
||||
def household_id(self) -> UUID4 | None:
|
||||
return self._household_id
|
||||
|
||||
def _log_exception(self, e: Exception) -> None:
|
||||
self.logger.error(f"Error processing query for Repo model={self.model.__name__} schema={self.schema.__name__}")
|
||||
@@ -70,6 +79,8 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
|
||||
if self.group_id:
|
||||
dct["group_id"] = self.group_id
|
||||
if self.household_id:
|
||||
dct["household_id"] = self.household_id
|
||||
|
||||
return {**dct, **kwargs}
|
||||
|
||||
@@ -341,7 +352,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
self.logger.error(e)
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
|
||||
count_query = select(func.count()).select_from(query)
|
||||
count_query = select(func.count()).select_from(query.subquery())
|
||||
count = self.session.scalar(count_query)
|
||||
if not count:
|
||||
count = 0
|
||||
@@ -373,15 +384,15 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
order_dir: OrderDirection,
|
||||
order_by_null: OrderByNullPosition | None,
|
||||
) -> Select:
|
||||
# queries handle uppercase and lowercase differently, which is undesirable
|
||||
if isinstance(order_attr.type, sqltypes.String):
|
||||
order_attr = func.lower(order_attr)
|
||||
|
||||
if order_dir is OrderDirection.asc:
|
||||
order_attr = order_attr.asc()
|
||||
elif order_dir is OrderDirection.desc:
|
||||
order_attr = order_attr.desc()
|
||||
|
||||
# queries handle uppercase and lowercase differently, which is undesirable
|
||||
if isinstance(order_attr.type, sqltypes.String):
|
||||
order_attr = func.lower(order_attr)
|
||||
|
||||
if order_by_null is OrderByNullPosition.first:
|
||||
order_attr = nulls_first(order_attr)
|
||||
elif order_by_null is OrderByNullPosition.last:
|
||||
@@ -435,3 +446,40 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
def add_search_to_query(self, query: Select, schema: type[Schema], search: str) -> Select:
|
||||
search_filter = SearchFilter(self.session, search, schema._normalize_search)
|
||||
return search_filter.filter_query_by_search(query, schema, self.model)
|
||||
|
||||
|
||||
class GroupRepositoryGeneric(RepositoryGeneric[Schema, Model]):
|
||||
def __init__(
|
||||
self,
|
||||
session: Session,
|
||||
primary_key: str,
|
||||
sql_model: type[Model],
|
||||
schema: type[Schema],
|
||||
*,
|
||||
group_id: UUID4 | None | NotSet,
|
||||
) -> None:
|
||||
super().__init__(session, primary_key, sql_model, schema)
|
||||
if group_id is NOT_SET:
|
||||
raise ValueError("group_id must be set")
|
||||
self._group_id = group_id if group_id else None
|
||||
|
||||
|
||||
class HouseholdRepositoryGeneric(RepositoryGeneric[Schema, Model]):
|
||||
def __init__(
|
||||
self,
|
||||
session: Session,
|
||||
primary_key: str,
|
||||
sql_model: type[Model],
|
||||
schema: type[Schema],
|
||||
*,
|
||||
group_id: UUID4 | None | NotSet,
|
||||
household_id: UUID4 | None | NotSet,
|
||||
) -> None:
|
||||
super().__init__(session, primary_key, sql_model, schema)
|
||||
if group_id is NOT_SET:
|
||||
raise ValueError("group_id must be set")
|
||||
self._group_id = group_id if group_id else None
|
||||
|
||||
if household_id is NOT_SET:
|
||||
raise ValueError("household_id must be set")
|
||||
self._household_id = household_id if household_id else None
|
||||
|
||||
@@ -4,19 +4,12 @@ from uuid import UUID
|
||||
|
||||
from pydantic import UUID4
|
||||
from slugify import slugify
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from mealie.db.models.group import Group
|
||||
from mealie.db.models.recipe.category import Category
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
from mealie.db.models.recipe.tag import Tag
|
||||
from mealie.db.models.recipe.tool import Tool
|
||||
from mealie.db.models.users.users import User
|
||||
from mealie.schema.group.group_statistics import GroupStatistics
|
||||
from mealie.schema.user.user import GroupBase, GroupInDB, UpdateGroup
|
||||
|
||||
from ..db.models._model_base import SqlAlchemyBase
|
||||
from .repository_generic import RepositoryGeneric
|
||||
|
||||
|
||||
@@ -74,16 +67,3 @@ class RepositoryGroup(RepositoryGeneric[GroupInDB, Group]):
|
||||
return self.get_one(slug_or_id)
|
||||
else:
|
||||
return self.get_one(slug_or_id, key="slug")
|
||||
|
||||
def statistics(self, group_id: UUID4) -> GroupStatistics:
|
||||
def model_count(model: type[SqlAlchemyBase]) -> int:
|
||||
stmt = select(func.count(model.id)).filter_by(group_id=group_id)
|
||||
return self.session.scalar(stmt)
|
||||
|
||||
return GroupStatistics(
|
||||
total_recipes=model_count(RecipeModel),
|
||||
total_users=model_count(User),
|
||||
total_categories=model_count(Category),
|
||||
total_tags=model_count(Tag),
|
||||
total_tools=model_count(Tool),
|
||||
)
|
||||
|
||||
103
mealie/repos/repository_household.py
Normal file
103
mealie/repos/repository_household.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from collections.abc import Iterable
|
||||
from typing import cast
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import UUID4
|
||||
from slugify import slugify
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from mealie.db.models._model_base import SqlAlchemyBase
|
||||
from mealie.db.models.household.household import Household
|
||||
from mealie.db.models.recipe.category import Category
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
from mealie.db.models.recipe.tag import Tag
|
||||
from mealie.db.models.recipe.tool import Tool
|
||||
from mealie.db.models.users.users import User
|
||||
from mealie.repos.repository_generic import GroupRepositoryGeneric
|
||||
from mealie.schema.household.household import HouseholdCreate, HouseholdInDB, UpdateHousehold
|
||||
from mealie.schema.household.household_statistics import HouseholdStatistics
|
||||
|
||||
|
||||
class RepositoryHousehold(GroupRepositoryGeneric[HouseholdInDB, Household]):
|
||||
def create(self, data: HouseholdCreate | dict) -> HouseholdInDB:
|
||||
if isinstance(data, HouseholdCreate):
|
||||
data = data.model_dump()
|
||||
|
||||
if not data.get("group_id"):
|
||||
data["group_id"] = self.group_id
|
||||
max_attempts = 10
|
||||
original_name = cast(str, data["name"])
|
||||
|
||||
attempts = 0
|
||||
while True:
|
||||
try:
|
||||
data["slug"] = slugify(data["name"])
|
||||
return super().create(data)
|
||||
except IntegrityError:
|
||||
self.session.rollback()
|
||||
attempts += 1
|
||||
if attempts >= max_attempts:
|
||||
raise
|
||||
|
||||
data["name"] = f"{original_name} ({attempts})"
|
||||
|
||||
def create_many(self, data: Iterable[HouseholdInDB | dict]) -> list[HouseholdInDB]:
|
||||
# since create uses special logic for resolving slugs, we don't want to use the standard create_many method
|
||||
return [self.create(new_household) for new_household in data]
|
||||
|
||||
def update(self, match_value: str | int | UUID4, new_data: UpdateHousehold | dict) -> HouseholdInDB:
|
||||
if isinstance(new_data, HouseholdCreate):
|
||||
new_data.slug = slugify(new_data.name)
|
||||
else:
|
||||
new_data["slug"] = slugify(new_data["name"])
|
||||
|
||||
return super().update(match_value, new_data)
|
||||
|
||||
def update_many(self, data: Iterable[UpdateHousehold | dict]) -> list[HouseholdInDB]:
|
||||
# since update uses special logic for resolving slugs, we don't want to use the standard update_many method
|
||||
return [
|
||||
self.update(household["id"] if isinstance(household, dict) else household.id, household)
|
||||
for household in data
|
||||
]
|
||||
|
||||
def get_by_name(self, name: str) -> HouseholdInDB | None:
|
||||
if not self.group_id:
|
||||
raise Exception("group_id not set")
|
||||
dbhousehold = (
|
||||
self.session.execute(select(self.model).filter_by(name=name, group_id=self.group_id))
|
||||
.scalars()
|
||||
.one_or_none()
|
||||
)
|
||||
if dbhousehold is None:
|
||||
return None
|
||||
return self.schema.model_validate(dbhousehold)
|
||||
|
||||
def get_by_slug_or_id(self, slug_or_id: str | UUID) -> HouseholdInDB | None:
|
||||
if isinstance(slug_or_id, str):
|
||||
try:
|
||||
slug_or_id = UUID(slug_or_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if isinstance(slug_or_id, UUID):
|
||||
return self.get_one(slug_or_id)
|
||||
else:
|
||||
return self.get_one(slug_or_id, key="slug")
|
||||
|
||||
def statistics(self, group_id: UUID4, household_id: UUID4) -> HouseholdStatistics:
|
||||
def model_count(model: type[SqlAlchemyBase], *, filter_household: bool = True) -> int:
|
||||
stmt = select(func.count(model.id)).filter_by(group_id=group_id)
|
||||
if filter_household:
|
||||
stmt = stmt.filter_by(household_id=household_id)
|
||||
return self.session.scalar(stmt)
|
||||
|
||||
return HouseholdStatistics(
|
||||
# household-level statistics
|
||||
total_recipes=model_count(RecipeModel),
|
||||
total_users=model_count(User),
|
||||
# group-level statistics
|
||||
total_categories=model_count(Category, filter_household=False),
|
||||
total_tags=model_count(Tag, filter_household=False),
|
||||
total_tools=model_count(Tool, filter_household=False),
|
||||
)
|
||||
@@ -1,17 +1,12 @@
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import or_, select
|
||||
|
||||
from mealie.db.models.group.mealplan import GroupMealPlanRules
|
||||
from mealie.db.models.household.mealplan import GroupMealPlanRules
|
||||
from mealie.schema.meal_plan.plan_rules import PlanRulesDay, PlanRulesOut, PlanRulesType
|
||||
|
||||
from .repository_generic import RepositoryGeneric
|
||||
from .repository_generic import HouseholdRepositoryGeneric
|
||||
|
||||
|
||||
class RepositoryMealPlanRules(RepositoryGeneric[PlanRulesOut, GroupMealPlanRules]):
|
||||
def by_group(self, group_id: UUID) -> "RepositoryMealPlanRules":
|
||||
return super().by_group(group_id)
|
||||
|
||||
class RepositoryMealPlanRules(HouseholdRepositoryGeneric[PlanRulesOut, GroupMealPlanRules]):
|
||||
def get_rules(self, day: PlanRulesDay, entry_type: PlanRulesType) -> list[PlanRulesOut]:
|
||||
stmt = select(GroupMealPlanRules).filter(
|
||||
or_(
|
||||
@@ -26,6 +21,11 @@ class RepositoryMealPlanRules(RepositoryGeneric[PlanRulesOut, GroupMealPlanRules
|
||||
),
|
||||
)
|
||||
|
||||
if self.group_id:
|
||||
stmt = stmt.filter(GroupMealPlanRules.group_id == self.group_id)
|
||||
if self.household_id:
|
||||
stmt = stmt.filter(GroupMealPlanRules.household_id == self.household_id)
|
||||
|
||||
rules = self.session.execute(stmt).scalars().all()
|
||||
|
||||
return [self.schema.model_validate(x) for x in rules]
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
from datetime import datetime, timezone
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from mealie.db.models.group import GroupMealPlan
|
||||
from mealie.db.models.household import GroupMealPlan
|
||||
from mealie.schema.meal_plan.new_meal import ReadPlanEntry
|
||||
|
||||
from .repository_generic import RepositoryGeneric
|
||||
from .repository_generic import HouseholdRepositoryGeneric
|
||||
|
||||
|
||||
class RepositoryMeals(RepositoryGeneric[ReadPlanEntry, GroupMealPlan]):
|
||||
def by_group(self, group_id: UUID) -> "RepositoryMeals":
|
||||
return super().by_group(group_id)
|
||||
class RepositoryMeals(HouseholdRepositoryGeneric[ReadPlanEntry, GroupMealPlan]):
|
||||
def get_today(self) -> list[ReadPlanEntry]:
|
||||
if not self.household_id:
|
||||
raise Exception("household_id not set")
|
||||
|
||||
def get_today(self, group_id: UUID) -> list[ReadPlanEntry]:
|
||||
today = datetime.now(tz=timezone.utc).date()
|
||||
stmt = select(GroupMealPlan).filter(GroupMealPlan.date == today, GroupMealPlan.group_id == group_id)
|
||||
stmt = select(GroupMealPlan).filter(
|
||||
GroupMealPlan.date == today, GroupMealPlan.household_id == self.household_id
|
||||
)
|
||||
plans = self.session.execute(stmt).scalars().all()
|
||||
return [self.schema.model_validate(x) for x in plans]
|
||||
|
||||
@@ -8,11 +8,11 @@ from pydantic import UUID4
|
||||
from slugify import slugify
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import InstrumentedAttribute, joinedload
|
||||
from typing_extensions import Self
|
||||
|
||||
from mealie.db.models.recipe.category import Category
|
||||
from mealie.db.models.recipe.ingredient import RecipeIngredientModel
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
from mealie.db.models.recipe.settings import RecipeSettings
|
||||
from mealie.db.models.recipe.tag import Tag
|
||||
from mealie.db.models.recipe.tool import Tool
|
||||
from mealie.db.models.users.user_to_recipe import UserToRecipe
|
||||
@@ -34,10 +34,17 @@ from mealie.schema.response.pagination import (
|
||||
|
||||
from ..db.models._model_base import SqlAlchemyBase
|
||||
from ..schema._mealie.mealie_model import extract_uuids
|
||||
from .repository_generic import RepositoryGeneric
|
||||
from .repository_generic import HouseholdRepositoryGeneric
|
||||
|
||||
|
||||
class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
|
||||
user_id: UUID4 | None = None
|
||||
|
||||
def by_user(self: Self, user_id: UUID4) -> Self:
|
||||
"""Add a user_id to the repo, which will be used to handle recipe ratings"""
|
||||
self.user_id = user_id
|
||||
return self
|
||||
|
||||
def create(self, document: Recipe) -> Recipe: # type: ignore
|
||||
max_retries = 10
|
||||
original_name: str = document.name # type: ignore
|
||||
@@ -53,33 +60,6 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
if i >= max_retries:
|
||||
raise
|
||||
|
||||
def by_group(self, group_id: UUID) -> "RepositoryRecipes":
|
||||
return super().by_group(group_id)
|
||||
|
||||
def get_all_public(self, limit: int | None = None, order_by: str | None = None, start=0, override_schema=None):
|
||||
eff_schema = override_schema or self.schema
|
||||
|
||||
if order_by:
|
||||
order_attr = getattr(self.model, str(order_by))
|
||||
stmt = (
|
||||
sa.select(self.model)
|
||||
.join(RecipeSettings)
|
||||
.filter(RecipeSettings.public == True) # noqa: E712
|
||||
.order_by(order_attr.desc())
|
||||
.offset(start)
|
||||
.limit(limit)
|
||||
)
|
||||
return [eff_schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
|
||||
stmt = (
|
||||
sa.select(self.model)
|
||||
.join(RecipeSettings)
|
||||
.filter(RecipeSettings.public == True) # noqa: E712
|
||||
.offset(start)
|
||||
.limit(limit)
|
||||
)
|
||||
return [eff_schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
|
||||
def update_image(self, slug: str, _: str | None = None) -> int:
|
||||
entry: RecipeModel = self._query_one(match_value=slug)
|
||||
entry.image = randint(0, 255)
|
||||
@@ -103,44 +83,6 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
override_schema=override_schema,
|
||||
)
|
||||
|
||||
def summary(
|
||||
self, group_id, start=0, limit=99999, load_foods=False, order_by="created_at", order_descending=True
|
||||
) -> Sequence[RecipeModel]:
|
||||
args = [
|
||||
joinedload(RecipeModel.recipe_category),
|
||||
joinedload(RecipeModel.tags),
|
||||
joinedload(RecipeModel.tools),
|
||||
]
|
||||
|
||||
if load_foods:
|
||||
args.append(joinedload(RecipeModel.recipe_ingredient).options(joinedload(RecipeIngredientModel.food)))
|
||||
|
||||
try:
|
||||
if order_by:
|
||||
order_attr = getattr(RecipeModel, order_by)
|
||||
else:
|
||||
order_attr = RecipeModel.created_at
|
||||
|
||||
except AttributeError:
|
||||
self.logger.info(f'Attempted to sort by unknown sort property "{order_by}"; ignoring')
|
||||
order_attr = RecipeModel.created_at
|
||||
|
||||
if order_descending:
|
||||
order_attr = order_attr.desc()
|
||||
|
||||
else:
|
||||
order_attr = order_attr.asc()
|
||||
|
||||
stmt = (
|
||||
sa.select(RecipeModel)
|
||||
.options(*args)
|
||||
.filter(RecipeModel.group_id == group_id)
|
||||
.order_by(order_attr)
|
||||
.offset(start)
|
||||
.limit(limit)
|
||||
)
|
||||
return self.session.execute(stmt).scalars().all()
|
||||
|
||||
def _uuids_for_items(self, items: list[UUID | str] | None, model: type[SqlAlchemyBase]) -> list[UUID] | None:
|
||||
if not items:
|
||||
return None
|
||||
@@ -227,6 +169,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
joinedload(RecipeModel.recipe_category),
|
||||
joinedload(RecipeModel.tags),
|
||||
joinedload(RecipeModel.tools),
|
||||
joinedload(RecipeModel.user),
|
||||
]
|
||||
|
||||
q = q.options(*args)
|
||||
@@ -296,6 +239,11 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
.join(RecipeModel.recipe_category)
|
||||
.filter(RecipeModel.recipe_category.any(Category.id.in_(ids)))
|
||||
)
|
||||
if self.group_id:
|
||||
stmt = stmt.filter(RecipeModel.group_id == self.group_id)
|
||||
if self.household_id:
|
||||
stmt = stmt.filter(RecipeModel.household_id == self.household_id)
|
||||
|
||||
return [RecipeSummary.model_validate(x) for x in self.session.execute(stmt).unique().scalars().all()]
|
||||
|
||||
def _build_recipe_filter(
|
||||
@@ -309,12 +257,11 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
require_all_tools: bool = True,
|
||||
require_all_foods: bool = True,
|
||||
) -> list:
|
||||
fltr: list[sa.ColumnElement] = []
|
||||
if self.group_id:
|
||||
fltr = [
|
||||
RecipeModel.group_id == self.group_id,
|
||||
]
|
||||
else:
|
||||
fltr = []
|
||||
fltr.append(RecipeModel.group_id == self.group_id)
|
||||
if self.household_id:
|
||||
fltr.append(RecipeModel.household_id == self.household_id)
|
||||
|
||||
if categories:
|
||||
if require_all_categories:
|
||||
@@ -382,12 +329,12 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
return [self.schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
|
||||
def get_random(self, limit=1) -> list[Recipe]:
|
||||
stmt = (
|
||||
sa.select(RecipeModel)
|
||||
.filter(RecipeModel.group_id == self.group_id)
|
||||
.order_by(sa.func.random()) # Postgres and SQLite specific
|
||||
.limit(limit)
|
||||
)
|
||||
stmt = sa.select(RecipeModel).order_by(sa.func.random()).limit(limit) # Postgres and SQLite specific
|
||||
if self.group_id:
|
||||
stmt = stmt.filter(RecipeModel.group_id == self.group_id)
|
||||
if self.household_id:
|
||||
stmt = stmt.filter(RecipeModel.household_id == self.household_id)
|
||||
|
||||
return [self.schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
|
||||
def get_by_slug(self, group_id: UUID4, slug: str) -> Recipe | None:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.db.models.group.shopping_list import ShoppingList
|
||||
from mealie.schema.group.group_shopping_list import ShoppingListOut, ShoppingListUpdate
|
||||
from mealie.db.models.household.shopping_list import ShoppingList
|
||||
from mealie.schema.household.group_shopping_list import ShoppingListOut, ShoppingListUpdate
|
||||
|
||||
from .repository_generic import RepositoryGeneric
|
||||
from .repository_generic import HouseholdRepositoryGeneric
|
||||
|
||||
|
||||
class RepositoryShoppingList(RepositoryGeneric[ShoppingListOut, ShoppingList]):
|
||||
class RepositoryShoppingList(HouseholdRepositoryGeneric[ShoppingListOut, ShoppingList]):
|
||||
def update(self, item_id: UUID4, data: ShoppingListUpdate) -> ShoppingListOut: # type: ignore
|
||||
return super().update(item_id, data)
|
||||
|
||||
@@ -4,10 +4,10 @@ from sqlalchemy import select
|
||||
from mealie.db.models.recipe.ingredient import IngredientUnitModel
|
||||
from mealie.schema.recipe.recipe_ingredient import IngredientUnit
|
||||
|
||||
from .repository_generic import RepositoryGeneric
|
||||
from .repository_generic import GroupRepositoryGeneric
|
||||
|
||||
|
||||
class RepositoryUnit(RepositoryGeneric[IngredientUnit, IngredientUnitModel]):
|
||||
class RepositoryUnit(GroupRepositoryGeneric[IngredientUnit, IngredientUnitModel]):
|
||||
def _get_unit(self, id: UUID4) -> IngredientUnitModel:
|
||||
stmt = select(self.model).filter_by(**self._filter_builder(**{"id": id}))
|
||||
return self.session.execute(stmt).scalars().one()
|
||||
@@ -26,6 +26,3 @@ class RepositoryUnit(RepositoryGeneric[IngredientUnit, IngredientUnitModel]):
|
||||
raise e
|
||||
|
||||
return self.get_one(to_unit)
|
||||
|
||||
def by_group(self, group_id: UUID4) -> "RepositoryUnit":
|
||||
return super().by_group(group_id)
|
||||
|
||||
@@ -10,12 +10,12 @@ from mealie.db.models.users.user_to_recipe import UserToRecipe
|
||||
from mealie.schema.user.user import PrivateUser, UserRatingOut
|
||||
|
||||
from ..db.models.users import User
|
||||
from .repository_generic import RepositoryGeneric
|
||||
from .repository_generic import GroupRepositoryGeneric
|
||||
|
||||
settings = get_app_settings()
|
||||
|
||||
|
||||
class RepositoryUsers(RepositoryGeneric[PrivateUser, User]):
|
||||
class RepositoryUsers(GroupRepositoryGeneric[PrivateUser, User]):
|
||||
def update_password(self, id, password: str):
|
||||
entry = self._query_one(match_value=id)
|
||||
if settings.IS_DEMO:
|
||||
@@ -75,7 +75,10 @@ class RepositoryUsers(RepositoryGeneric[PrivateUser, User]):
|
||||
return [self.schema.model_validate(x) for x in results]
|
||||
|
||||
|
||||
class RepositoryUserRatings(RepositoryGeneric[UserRatingOut, UserToRecipe]):
|
||||
class RepositoryUserRatings(GroupRepositoryGeneric[UserRatingOut, UserToRecipe]):
|
||||
# Since users can post events on recipes that belong to other households,
|
||||
# this is a group repository, rather than a household repository.
|
||||
|
||||
def get_by_user(self, user_id: UUID4, favorites_only=False) -> list[UserRatingOut]:
|
||||
stmt = select(UserToRecipe).filter(UserToRecipe.user_id == user_id)
|
||||
if favorites_only:
|
||||
|
||||
@@ -2,8 +2,6 @@ from abc import ABC, abstractmethod
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.repos.repository_factory import AllRepositories
|
||||
|
||||
@@ -13,14 +11,13 @@ class AbstractSeeder(ABC):
|
||||
Abstract class for seeding data.
|
||||
"""
|
||||
|
||||
def __init__(self, db: AllRepositories, logger: Logger | None = None, group_id: UUID4 | None = None):
|
||||
def __init__(self, db: AllRepositories, logger: Logger | None = None):
|
||||
"""
|
||||
Initialize the abstract seeder.
|
||||
:param db_conn: Database connection.
|
||||
:param logger: Logger.
|
||||
"""
|
||||
self.repos = db
|
||||
self.group_id = group_id
|
||||
self.logger = logger or get_logger("Data Seeder")
|
||||
self.resources = Path(__file__).parent / "resources"
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ def dev_users() -> list[dict]:
|
||||
"email": "jason@example.com",
|
||||
"password": hash_password(settings._DEFAULT_PASSWORD),
|
||||
"group": settings.DEFAULT_GROUP,
|
||||
"household": settings.DEFAULT_HOUSEHOLD,
|
||||
"admin": False,
|
||||
},
|
||||
{
|
||||
@@ -23,6 +24,7 @@ def dev_users() -> list[dict]:
|
||||
"email": "bob@example.com",
|
||||
"password": hash_password(settings._DEFAULT_PASSWORD),
|
||||
"group": settings.DEFAULT_GROUP,
|
||||
"household": settings.DEFAULT_HOUSEHOLD,
|
||||
"admin": False,
|
||||
},
|
||||
{
|
||||
@@ -31,6 +33,7 @@ def dev_users() -> list[dict]:
|
||||
"email": "sarah@example.com",
|
||||
"password": hash_password(settings._DEFAULT_PASSWORD),
|
||||
"group": settings.DEFAULT_GROUP,
|
||||
"household": settings.DEFAULT_HOUSEHOLD,
|
||||
"admin": False,
|
||||
},
|
||||
{
|
||||
@@ -39,6 +42,7 @@ def dev_users() -> list[dict]:
|
||||
"email": "sammy@example.com",
|
||||
"password": hash_password(settings._DEFAULT_PASSWORD),
|
||||
"group": settings.DEFAULT_GROUP,
|
||||
"household": settings.DEFAULT_HOUSEHOLD,
|
||||
"admin": False,
|
||||
},
|
||||
]
|
||||
@@ -51,6 +55,7 @@ def default_user_init(db: AllRepositories):
|
||||
"email": settings._DEFAULT_EMAIL,
|
||||
"password": hash_password(settings._DEFAULT_PASSWORD),
|
||||
"group": settings.DEFAULT_GROUP,
|
||||
"household": settings.DEFAULT_HOUSEHOLD,
|
||||
"admin": True,
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from .resources import foods, labels, units
|
||||
class MultiPurposeLabelSeeder(AbstractSeeder):
|
||||
@cached_property
|
||||
def service(self):
|
||||
return MultiPurposeLabelService(self.repos, self.group_id)
|
||||
return MultiPurposeLabelService(self.repos)
|
||||
|
||||
def get_file(self, locale: str | None = None) -> pathlib.Path:
|
||||
locale_path = self.resources / "labels" / "locales" / f"{locale}.json"
|
||||
@@ -31,7 +31,7 @@ class MultiPurposeLabelSeeder(AbstractSeeder):
|
||||
seen_label_names.add(label["name"])
|
||||
yield MultiPurposeLabelSave(
|
||||
name=label["name"],
|
||||
group_id=self.group_id,
|
||||
group_id=self.repos.group_id,
|
||||
)
|
||||
|
||||
def seed(self, locale: str | None = None) -> None:
|
||||
@@ -58,7 +58,7 @@ class IngredientUnitsSeeder(AbstractSeeder):
|
||||
|
||||
seen_unit_names.add(unit["name"])
|
||||
yield SaveIngredientUnit(
|
||||
group_id=self.group_id,
|
||||
group_id=self.repos.group_id,
|
||||
name=unit["name"],
|
||||
plural_name=unit.get("plural_name"),
|
||||
description=unit["description"],
|
||||
@@ -86,7 +86,7 @@ class IngredientFoodsSeeder(AbstractSeeder):
|
||||
seed_foods: dict[str, str] = json.loads(file.read_text(encoding="utf-8"))
|
||||
for food in set(seed_foods.values()):
|
||||
yield SaveIngredientFood(
|
||||
group_id=self.group_id,
|
||||
group_id=self.repos.group_id,
|
||||
name=food,
|
||||
description="",
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ from . import (
|
||||
comments,
|
||||
explore,
|
||||
groups,
|
||||
households,
|
||||
organizers,
|
||||
parser,
|
||||
recipe,
|
||||
@@ -21,6 +22,7 @@ router = APIRouter(prefix="/api")
|
||||
router.include_router(app.router)
|
||||
router.include_router(auth.router)
|
||||
router.include_router(users.router)
|
||||
router.include_router(households.router)
|
||||
router.include_router(groups.router)
|
||||
router.include_router(recipe.router)
|
||||
router.include_router(organizers.router)
|
||||
|
||||
@@ -6,7 +6,12 @@ from pydantic import UUID4, ConfigDict
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from mealie.core.config import get_app_dirs, get_app_settings
|
||||
from mealie.core.dependencies.dependencies import get_admin_user, get_current_user, get_integration_id, get_public_group
|
||||
from mealie.core.dependencies.dependencies import (
|
||||
get_admin_user,
|
||||
get_current_user,
|
||||
get_integration_id,
|
||||
get_public_group,
|
||||
)
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.core.settings.directories import AppDirectories
|
||||
@@ -14,8 +19,10 @@ from mealie.core.settings.settings import AppSettings
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.lang import local_provider
|
||||
from mealie.lang.providers import Translator
|
||||
from mealie.repos.all_repositories import AllRepositories
|
||||
from mealie.repos._utils import NOT_SET, NotSet
|
||||
from mealie.repos.all_repositories import AllRepositories, get_repositories
|
||||
from mealie.routes._base.checks import OperationChecks
|
||||
from mealie.schema.household.household import HouseholdInDB
|
||||
from mealie.schema.user.user import GroupInDB, PrivateUser
|
||||
from mealie.services.event_bus_service.event_bus_service import EventBusService
|
||||
from mealie.services.event_bus_service.event_types import EventDocumentDataBase, EventTypes
|
||||
@@ -37,7 +44,7 @@ class _BaseController(ABC): # noqa: B024
|
||||
@property
|
||||
def repos(self):
|
||||
if not self._repos:
|
||||
self._repos = AllRepositories(self.session)
|
||||
self._repos = AllRepositories(self.session, group_id=self.group_id, household_id=self.household_id)
|
||||
return self._repos
|
||||
|
||||
@property
|
||||
@@ -58,6 +65,14 @@ class _BaseController(ABC): # noqa: B024
|
||||
self._folders = get_app_dirs()
|
||||
return self._folders
|
||||
|
||||
@property
|
||||
def group_id(self) -> UUID4 | None | NotSet:
|
||||
return NOT_SET
|
||||
|
||||
@property
|
||||
def household_id(self) -> UUID4 | None | NotSet:
|
||||
return NOT_SET
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
@@ -71,15 +86,40 @@ class BasePublicController(_BaseController):
|
||||
...
|
||||
|
||||
|
||||
class BasePublicExploreController(BasePublicController):
|
||||
class BasePublicGroupExploreController(BasePublicController):
|
||||
"""
|
||||
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.
|
||||
Base class for all controllers that are public and explore group data.
|
||||
"""
|
||||
|
||||
group: GroupInDB = Depends(get_public_group)
|
||||
|
||||
@property
|
||||
def group_id(self) -> UUID4 | None | NotSet:
|
||||
return self.group.id
|
||||
|
||||
def get_explore_url_path(self, endpoint: str) -> str:
|
||||
if endpoint.startswith("/"):
|
||||
endpoint = endpoint[1:]
|
||||
return f"/explore/groups/{self.group.slug}/{endpoint}"
|
||||
|
||||
|
||||
class BasePublicHouseholdExploreController(BasePublicGroupExploreController):
|
||||
"""
|
||||
Base class for all controllers that are public and explore household data.
|
||||
"""
|
||||
|
||||
@property
|
||||
def cross_household_repos(self):
|
||||
"""
|
||||
Household-level repos with no household filter. Public controllers don't have access to a household identifier;
|
||||
instead, they return all public data, filtered by the household preferences.
|
||||
|
||||
When using this repo, the caller should filter by household preferences, e.g.:
|
||||
|
||||
`household.preferences.privateHousehold = FALSE`
|
||||
"""
|
||||
return get_repositories(self.session, group_id=self.group_id, household_id=None)
|
||||
|
||||
|
||||
class BaseUserController(_BaseController):
|
||||
"""
|
||||
@@ -105,10 +145,18 @@ class BaseUserController(_BaseController):
|
||||
def group_id(self) -> UUID4:
|
||||
return self.user.group_id
|
||||
|
||||
@property
|
||||
def household_id(self) -> UUID4:
|
||||
return self.user.household_id
|
||||
|
||||
@property
|
||||
def group(self) -> GroupInDB:
|
||||
return self.repos.groups.get_one(self.group_id)
|
||||
|
||||
@property
|
||||
def household(self) -> HouseholdInDB:
|
||||
return self.repos.households.get_one(self.household_id)
|
||||
|
||||
@property
|
||||
def checks(self) -> OperationChecks:
|
||||
if not self._checks:
|
||||
@@ -125,6 +173,13 @@ class BaseAdminController(BaseUserController):
|
||||
|
||||
user: PrivateUser = Depends(get_admin_user)
|
||||
|
||||
@property
|
||||
def repos(self):
|
||||
if not self._repos:
|
||||
# Admins have access to all groups and households, so we don't want to filter by group_id or household_id
|
||||
self._repos = AllRepositories(self.session, group_id=None, household_id=None)
|
||||
return self._repos
|
||||
|
||||
|
||||
class BaseCrudController(BaseUserController):
|
||||
"""
|
||||
@@ -133,10 +188,18 @@ class BaseCrudController(BaseUserController):
|
||||
|
||||
event_bus: EventBusService = Depends(EventBusService.as_dependency)
|
||||
|
||||
def publish_event(self, event_type: EventTypes, document_data: EventDocumentDataBase, message: str = "") -> None:
|
||||
def publish_event(
|
||||
self,
|
||||
event_type: EventTypes,
|
||||
document_data: EventDocumentDataBase,
|
||||
group_id: UUID4,
|
||||
household_id: UUID4 | None,
|
||||
message: str = "",
|
||||
) -> None:
|
||||
self.event_bus.dispatch(
|
||||
integration_id=self.integration_id,
|
||||
group_id=self.group_id,
|
||||
group_id=group_id,
|
||||
household_id=household_id,
|
||||
event_type=event_type,
|
||||
document_data=document_data,
|
||||
message=message,
|
||||
|
||||
@@ -35,7 +35,7 @@ class MealieCrudRoute(APIRoute):
|
||||
response = await original_route_handler(request)
|
||||
response_body = json.loads(response.body)
|
||||
if isinstance(response_body, dict):
|
||||
if last_modified := response_body.get("updateAt"):
|
||||
if last_modified := response_body.get("updatedAt"):
|
||||
response.headers["last-modified"] = last_modified
|
||||
|
||||
# Force no-cache for all responses to prevent browser from caching API calls
|
||||
|
||||
@@ -2,11 +2,11 @@ from mealie.routes._base.routers import AdminAPIRouter
|
||||
|
||||
from . import (
|
||||
admin_about,
|
||||
admin_analytics,
|
||||
admin_backups,
|
||||
admin_email,
|
||||
admin_maintenance,
|
||||
admin_management_groups,
|
||||
admin_management_households,
|
||||
admin_management_users,
|
||||
)
|
||||
|
||||
@@ -14,8 +14,8 @@ router = AdminAPIRouter(prefix="/admin")
|
||||
|
||||
router.include_router(admin_about.router, tags=["Admin: About"])
|
||||
router.include_router(admin_management_users.router, tags=["Admin: Manage Users"])
|
||||
router.include_router(admin_management_households.router, tags=["Admin: Manage Households"])
|
||||
router.include_router(admin_management_groups.router, tags=["Admin: Manage Groups"])
|
||||
router.include_router(admin_email.router, tags=["Admin: Email"])
|
||||
router.include_router(admin_backups.router, tags=["Admin: Backups"])
|
||||
router.include_router(admin_maintenance.router, tags=["Admin: Maintenance"])
|
||||
router.include_router(admin_analytics.router, tags=["Admin: Analytics"])
|
||||
|
||||
@@ -27,6 +27,7 @@ class AdminAboutController(BaseAdminController):
|
||||
db_type=settings.DB_ENGINE,
|
||||
db_url=settings.DB_URL_PUBLIC,
|
||||
default_group=settings.DEFAULT_GROUP,
|
||||
default_household=settings.DEFAULT_HOUSEHOLD,
|
||||
allow_signup=settings.ALLOW_SIGNUP,
|
||||
build_id=settings.GIT_COMMIT_HASH,
|
||||
recipe_scraper_version=recipe_scraper_version.__version__,
|
||||
@@ -44,6 +45,7 @@ class AdminAboutController(BaseAdminController):
|
||||
uncategorized_recipes=self.repos.recipes.count_uncategorized(), # type: ignore
|
||||
untagged_recipes=self.repos.recipes.count_untagged(), # type: ignore
|
||||
total_users=self.repos.users.count_all(),
|
||||
total_households=self.repos.households.count_all(),
|
||||
total_groups=self.repos.groups.count_all(),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from mealie.routes._base import BaseAdminController, controller
|
||||
from mealie.schema.analytics.analytics import MealieAnalytics
|
||||
from mealie.services.analytics.service_analytics import AnalyticsService
|
||||
|
||||
router = APIRouter(prefix="/analytics", include_in_schema=False) # deprecated - use statistics route instead
|
||||
|
||||
|
||||
@controller(router)
|
||||
class AdminAboutController(BaseAdminController):
|
||||
@cached_property
|
||||
def service(self) -> AnalyticsService:
|
||||
return AnalyticsService(self.repos)
|
||||
|
||||
@router.get("", response_model=MealieAnalytics)
|
||||
def get_analytics(self):
|
||||
return self.service.calculate_analytics()
|
||||
@@ -13,11 +13,11 @@ from mealie.services.group_services.group_service import GroupService
|
||||
from .._base import BaseAdminController, controller
|
||||
from .._base.mixins import HttpRepo
|
||||
|
||||
router = APIRouter(prefix="/groups", tags=["Admin: Groups"])
|
||||
router = APIRouter(prefix="/groups")
|
||||
|
||||
|
||||
@controller(router)
|
||||
class AdminUserManagementRoutes(BaseAdminController):
|
||||
class AdminGroupManagementRoutes(BaseAdminController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
if not self.user:
|
||||
@@ -64,6 +64,7 @@ class AdminUserManagementRoutes(BaseAdminController):
|
||||
group.preferences = self.repos.group_preferences.update(item_id, preferences)
|
||||
|
||||
if data.name not in ["", group.name]:
|
||||
# only update the group if the name changed, since the name is the only field that can be updated
|
||||
group.name = data.name
|
||||
group = self.repo.update(item_id, group)
|
||||
|
||||
|
||||
91
mealie/routes/admin/admin_management_households.py
Normal file
91
mealie/routes/admin/admin_management_households.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import UUID4
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from mealie.db.models.users.users import User
|
||||
from mealie.schema.household.household import (
|
||||
HouseholdCreate,
|
||||
HouseholdInDB,
|
||||
HouseholdPagination,
|
||||
UpdateHouseholdAdmin,
|
||||
)
|
||||
from mealie.schema.mapper import mapper
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
from mealie.schema.response.responses import ErrorResponse
|
||||
from mealie.services.household_services.household_service import HouseholdService
|
||||
|
||||
from .._base import BaseAdminController, controller
|
||||
from .._base.mixins import HttpRepo
|
||||
|
||||
router = APIRouter(prefix="/households")
|
||||
|
||||
|
||||
@controller(router)
|
||||
class AdminHouseholdManagementRoutes(BaseAdminController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
if not self.user:
|
||||
raise Exception("No user is logged in.")
|
||||
|
||||
return self.repos.households
|
||||
|
||||
# =======================================================================
|
||||
# CRUD Operations
|
||||
|
||||
@property
|
||||
def mixins(self):
|
||||
return HttpRepo[HouseholdCreate, HouseholdInDB, UpdateHouseholdAdmin](
|
||||
self.repo,
|
||||
self.logger,
|
||||
self.registered_exceptions,
|
||||
)
|
||||
|
||||
@router.get("", response_model=HouseholdPagination)
|
||||
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||
response = self.repo.page_all(
|
||||
pagination=q,
|
||||
override=HouseholdInDB,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=HouseholdInDB, status_code=status.HTTP_201_CREATED)
|
||||
def create_one(self, data: HouseholdCreate):
|
||||
return HouseholdService.create_household(self.repos, data)
|
||||
|
||||
@router.get("/{item_id}", response_model=HouseholdInDB)
|
||||
def get_one(self, item_id: UUID4):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.put("/{item_id}", response_model=HouseholdInDB)
|
||||
def update_one(self, item_id: UUID4, data: UpdateHouseholdAdmin):
|
||||
household = self.repo.get_one(item_id)
|
||||
|
||||
if data.preferences:
|
||||
preferences = self.repos.household_preferences.get_one(value=item_id, key="household_id")
|
||||
preferences = mapper(data.preferences, preferences)
|
||||
household.preferences = self.repos.household_preferences.update(item_id, preferences)
|
||||
|
||||
if data.name not in ["", household.name]:
|
||||
# only update the household if the name changed, since the name is the only field that can be updated
|
||||
household.name = data.name
|
||||
household = self.repo.update(item_id, household)
|
||||
|
||||
return household
|
||||
|
||||
@router.delete("/{item_id}", response_model=HouseholdInDB)
|
||||
def delete_one(self, item_id: UUID4):
|
||||
item = self.repo.get_one(item_id)
|
||||
if item:
|
||||
stmt = select(func.count(User.id)).filter_by(group_id=item.group_id, household_id=item_id)
|
||||
user_count = self.session.scalar(stmt)
|
||||
if user_count:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorResponse.respond(message="Cannot delete household with users"),
|
||||
)
|
||||
|
||||
return self.mixins.delete_one(item_id)
|
||||
@@ -14,7 +14,7 @@ from mealie.schema.user.user_passwords import ForgotPassword, PasswordResetToken
|
||||
from mealie.services.user_services.password_reset_service import PasswordResetService
|
||||
from mealie.services.user_services.user_service import UserService
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["Admin: Users"])
|
||||
router = APIRouter(prefix="/users")
|
||||
|
||||
|
||||
@controller(router)
|
||||
|
||||
@@ -16,12 +16,20 @@ def get_app_info(session: Session = Depends(generate_session)):
|
||||
"""Get general application information"""
|
||||
settings = get_app_settings()
|
||||
|
||||
repos = get_repositories(session)
|
||||
default_group = repos.groups.get_by_name(settings.DEFAULT_GROUP)
|
||||
public_repos = get_repositories(session, group_id=None, household_id=None)
|
||||
|
||||
default_group_slug: str | None = None
|
||||
default_household_slug: str | None = None
|
||||
|
||||
default_group = public_repos.groups.get_by_name(settings.DEFAULT_GROUP)
|
||||
if default_group and default_group.preferences and not default_group.preferences.private_group:
|
||||
default_group_slug = default_group.slug
|
||||
else:
|
||||
default_group_slug = None
|
||||
|
||||
if default_group and default_group_slug:
|
||||
group_repos = get_repositories(session, group_id=default_group.id, household_id=None)
|
||||
default_household = group_repos.households.get_by_name(settings.DEFAULT_HOUSEHOLD)
|
||||
if default_household and default_household.preferences and not default_household.preferences.private_household:
|
||||
default_household_slug = default_household.slug
|
||||
|
||||
return AppInfo(
|
||||
version=APP_VERSION,
|
||||
@@ -29,6 +37,7 @@ def get_app_info(session: Session = Depends(generate_session)):
|
||||
production=settings.PRODUCTION,
|
||||
allow_signup=settings.ALLOW_SIGNUP,
|
||||
default_group_slug=default_group_slug,
|
||||
default_household_slug=default_household_slug,
|
||||
enable_oidc=settings.OIDC_READY,
|
||||
oidc_redirect=settings.OIDC_AUTO_REDIRECT,
|
||||
oidc_provider_name=settings.OIDC_PROVIDER_NAME,
|
||||
|
||||
@@ -7,13 +7,14 @@ from . import (
|
||||
controller_public_recipes,
|
||||
)
|
||||
|
||||
prefix = "/explore"
|
||||
router = APIRouter(prefix="/explore/groups/{group_slug}")
|
||||
|
||||
router = APIRouter()
|
||||
# group
|
||||
router.include_router(controller_public_foods.router, tags=["Explore: Foods"])
|
||||
router.include_router(controller_public_organizers.categories_router, tags=["Explore: Categories"])
|
||||
router.include_router(controller_public_organizers.tags_router, tags=["Explore: Tags"])
|
||||
router.include_router(controller_public_organizers.tools_router, tags=["Explore: Tools"])
|
||||
|
||||
router.include_router(controller_public_cookbooks.router, prefix=prefix, tags=["Explore: Cookbooks"])
|
||||
router.include_router(controller_public_foods.router, prefix=prefix, tags=["Explore: Foods"])
|
||||
router.include_router(controller_public_organizers.categories_router, prefix=prefix, tags=["Explore: Categories"])
|
||||
router.include_router(controller_public_organizers.tags_router, prefix=prefix, tags=["Explore: Tags"])
|
||||
router.include_router(controller_public_organizers.tools_router, prefix=prefix, tags=["Explore: Tools"])
|
||||
router.include_router(controller_public_recipes.router, prefix=prefix, tags=["Explore: Recipes"])
|
||||
# household
|
||||
router.include_router(controller_public_cookbooks.router, tags=["Explore: Cookbooks"])
|
||||
router.include_router(controller_public_recipes.router, tags=["Explore: Recipes"])
|
||||
|
||||
@@ -3,46 +3,46 @@ from uuid import UUID
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes._base import controller
|
||||
from mealie.routes._base.base_controllers import BasePublicExploreController
|
||||
from mealie.routes._base.base_controllers import BasePublicHouseholdExploreController
|
||||
from mealie.schema.cookbook.cookbook import ReadCookBook, RecipeCookBook
|
||||
from mealie.schema.make_dependable import make_dependable
|
||||
from mealie.schema.response.pagination import PaginationBase, PaginationQuery
|
||||
|
||||
router = APIRouter(prefix="/cookbooks/{group_slug}")
|
||||
router = APIRouter(prefix="/cookbooks")
|
||||
|
||||
|
||||
@controller(router)
|
||||
class PublicCookbooksController(BasePublicExploreController):
|
||||
class PublicCookbooksController(BasePublicHouseholdExploreController):
|
||||
@property
|
||||
def cookbooks(self):
|
||||
return self.repos.cookbooks.by_group(self.group.id)
|
||||
|
||||
@property
|
||||
def recipes(self):
|
||||
return self.repos.recipes.by_group(self.group.id)
|
||||
def cross_household_cookbooks(self):
|
||||
return self.cross_household_repos.cookbooks
|
||||
|
||||
@router.get("", response_model=PaginationBase[ReadCookBook])
|
||||
def get_all(
|
||||
self, q: PaginationQuery = Depends(make_dependable(PaginationQuery)), search: str | None = None
|
||||
self,
|
||||
q: PaginationQuery = Depends(make_dependable(PaginationQuery)),
|
||||
search: str | None = None,
|
||||
) -> PaginationBase[ReadCookBook]:
|
||||
public_filter = "public = TRUE"
|
||||
public_filter = "(household.preferences.privateHousehold = FALSE AND public = TRUE)"
|
||||
if q.query_filter:
|
||||
q.query_filter = f"({q.query_filter}) AND {public_filter}"
|
||||
else:
|
||||
q.query_filter = public_filter
|
||||
|
||||
response = self.cookbooks.page_all(
|
||||
response = self.cross_household_cookbooks.page_all(
|
||||
pagination=q,
|
||||
override=ReadCookBook,
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump())
|
||||
response.set_pagination_guides(self.get_explore_url_path(router.url_path_for("get_all")), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.get("/{item_id}", response_model=RecipeCookBook)
|
||||
def get_one(self, item_id: UUID4 | str) -> RecipeCookBook:
|
||||
NOT_FOUND_EXCEPTION = HTTPException(404, "cookbook not found")
|
||||
if isinstance(item_id, UUID):
|
||||
match_attr = "id"
|
||||
else:
|
||||
@@ -51,12 +51,24 @@ class PublicCookbooksController(BasePublicExploreController):
|
||||
match_attr = "id"
|
||||
except ValueError:
|
||||
match_attr = "slug"
|
||||
cookbook = self.cookbooks.get_one(item_id, match_attr)
|
||||
cookbook = self.cross_household_cookbooks.get_one(item_id, match_attr)
|
||||
|
||||
if not cookbook or not cookbook.public:
|
||||
raise HTTPException(404, "cookbook not found")
|
||||
raise NOT_FOUND_EXCEPTION
|
||||
household = self.repos.households.get_one(cookbook.household_id)
|
||||
if not household or household.preferences.private_household:
|
||||
raise NOT_FOUND_EXCEPTION
|
||||
|
||||
recipes = self.recipes.page_all(
|
||||
PaginationQuery(page=1, per_page=-1, query_filter="settings.public = TRUE"), cookbook=cookbook
|
||||
# limit recipes to only the household the cookbook belongs to
|
||||
recipes_repo = get_repositories(
|
||||
self.session, group_id=self.group_id, household_id=cookbook.household_id
|
||||
).recipes
|
||||
recipes = recipes_repo.page_all(
|
||||
PaginationQuery(
|
||||
page=1,
|
||||
per_page=-1,
|
||||
query_filter="settings.public = TRUE",
|
||||
),
|
||||
cookbook=cookbook,
|
||||
)
|
||||
return cookbook.cast(RecipeCookBook, recipes=recipes.items)
|
||||
|
||||
@@ -2,23 +2,25 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.routes._base import controller
|
||||
from mealie.routes._base.base_controllers import BasePublicExploreController
|
||||
from mealie.routes._base.base_controllers import BasePublicGroupExploreController
|
||||
from mealie.schema.make_dependable import make_dependable
|
||||
from mealie.schema.recipe.recipe_ingredient import IngredientFood
|
||||
from mealie.schema.response.pagination import PaginationBase, PaginationQuery
|
||||
|
||||
router = APIRouter(prefix="/foods/{group_slug}")
|
||||
router = APIRouter(prefix="/foods")
|
||||
|
||||
|
||||
@controller(router)
|
||||
class PublicFoodsController(BasePublicExploreController):
|
||||
class PublicFoodsController(BasePublicGroupExploreController):
|
||||
@property
|
||||
def ingredient_foods(self):
|
||||
return self.repos.ingredient_foods.by_group(self.group.id)
|
||||
return self.repos.ingredient_foods
|
||||
|
||||
@router.get("", response_model=PaginationBase[IngredientFood])
|
||||
def get_all(
|
||||
self, q: PaginationQuery = Depends(make_dependable(PaginationQuery)), search: str | None = None
|
||||
self,
|
||||
q: PaginationQuery = Depends(make_dependable(PaginationQuery)),
|
||||
search: str | None = None,
|
||||
) -> PaginationBase[IngredientFood]:
|
||||
response = self.ingredient_foods.page_all(
|
||||
pagination=q,
|
||||
@@ -26,7 +28,7 @@ class PublicFoodsController(BasePublicExploreController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump())
|
||||
response.set_pagination_guides(self.get_explore_url_path(router.url_path_for("get_all")), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.get("/{item_id}", response_model=IngredientFood)
|
||||
|
||||
@@ -2,28 +2,30 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.routes._base import controller
|
||||
from mealie.routes._base.base_controllers import BasePublicExploreController
|
||||
from mealie.routes._base.base_controllers import BasePublicGroupExploreController
|
||||
from mealie.schema.make_dependable import make_dependable
|
||||
from mealie.schema.recipe.recipe import RecipeCategory, RecipeTag, RecipeTool
|
||||
from mealie.schema.recipe.recipe_category import CategoryOut, TagOut
|
||||
from mealie.schema.recipe.recipe_tool import RecipeToolOut
|
||||
from mealie.schema.response.pagination import PaginationBase, PaginationQuery
|
||||
|
||||
base_prefix = "/organizers/{group_slug}"
|
||||
base_prefix = "/organizers"
|
||||
categories_router = APIRouter(prefix=f"{base_prefix}/categories")
|
||||
tags_router = APIRouter(prefix=f"{base_prefix}/tags")
|
||||
tools_router = APIRouter(prefix=f"{base_prefix}/tools")
|
||||
|
||||
|
||||
@controller(categories_router)
|
||||
class PublicCategoriesController(BasePublicExploreController):
|
||||
class PublicCategoriesController(BasePublicGroupExploreController):
|
||||
@property
|
||||
def categories(self):
|
||||
return self.repos.categories.by_group(self.group.id)
|
||||
return self.repos.categories
|
||||
|
||||
@categories_router.get("", response_model=PaginationBase[RecipeCategory])
|
||||
def get_all(
|
||||
self, q: PaginationQuery = Depends(make_dependable(PaginationQuery)), search: str | None = None
|
||||
self,
|
||||
q: PaginationQuery = Depends(make_dependable(PaginationQuery)),
|
||||
search: str | None = None,
|
||||
) -> PaginationBase[RecipeCategory]:
|
||||
response = self.categories.page_all(
|
||||
pagination=q,
|
||||
@@ -31,9 +33,7 @@ class PublicCategoriesController(BasePublicExploreController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(
|
||||
categories_router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump()
|
||||
)
|
||||
response.set_pagination_guides(self.get_explore_url_path(tags_router.url_path_for("get_all")), q.model_dump())
|
||||
return response
|
||||
|
||||
@categories_router.get("/{item_id}", response_model=CategoryOut)
|
||||
@@ -46,14 +46,16 @@ class PublicCategoriesController(BasePublicExploreController):
|
||||
|
||||
|
||||
@controller(tags_router)
|
||||
class PublicTagsController(BasePublicExploreController):
|
||||
class PublicTagsController(BasePublicGroupExploreController):
|
||||
@property
|
||||
def tags(self):
|
||||
return self.repos.tags.by_group(self.group.id)
|
||||
return self.repos.tags
|
||||
|
||||
@tags_router.get("", response_model=PaginationBase[RecipeTag])
|
||||
def get_all(
|
||||
self, q: PaginationQuery = Depends(make_dependable(PaginationQuery)), search: str | None = None
|
||||
self,
|
||||
q: PaginationQuery = Depends(make_dependable(PaginationQuery)),
|
||||
search: str | None = None,
|
||||
) -> PaginationBase[RecipeTag]:
|
||||
response = self.tags.page_all(
|
||||
pagination=q,
|
||||
@@ -61,7 +63,7 @@ class PublicTagsController(BasePublicExploreController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(tags_router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump())
|
||||
response.set_pagination_guides(self.get_explore_url_path(tags_router.url_path_for("get_all")), q.model_dump())
|
||||
return response
|
||||
|
||||
@tags_router.get("/{item_id}", response_model=TagOut)
|
||||
@@ -74,14 +76,16 @@ class PublicTagsController(BasePublicExploreController):
|
||||
|
||||
|
||||
@controller(tools_router)
|
||||
class PublicToolsController(BasePublicExploreController):
|
||||
class PublicToolsController(BasePublicGroupExploreController):
|
||||
@property
|
||||
def tools(self):
|
||||
return self.repos.tools.by_group(self.group.id)
|
||||
return self.repos.tools
|
||||
|
||||
@tools_router.get("", response_model=PaginationBase[RecipeTool])
|
||||
def get_all(
|
||||
self, q: PaginationQuery = Depends(make_dependable(PaginationQuery)), search: str | None = None
|
||||
self,
|
||||
q: PaginationQuery = Depends(make_dependable(PaginationQuery)),
|
||||
search: str | None = None,
|
||||
) -> PaginationBase[RecipeTool]:
|
||||
response = self.tools.page_all(
|
||||
pagination=q,
|
||||
@@ -89,7 +93,7 @@ class PublicToolsController(BasePublicExploreController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(tools_router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump())
|
||||
response.set_pagination_guides(self.get_explore_url_path(tools_router.url_path_for("get_all")), q.model_dump())
|
||||
return response
|
||||
|
||||
@tools_router.get("/{item_id}", response_model=RecipeToolOut)
|
||||
|
||||
@@ -4,8 +4,9 @@ import orjson
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes._base import controller
|
||||
from mealie.routes._base.base_controllers import BasePublicExploreController
|
||||
from mealie.routes._base.base_controllers import BasePublicHouseholdExploreController
|
||||
from mealie.routes.recipe.recipe_crud_routes import JSONBytes
|
||||
from mealie.schema.cookbook.cookbook import ReadCookBook
|
||||
from mealie.schema.make_dependable import make_dependable
|
||||
@@ -13,18 +14,18 @@ from mealie.schema.recipe import Recipe
|
||||
from mealie.schema.recipe.recipe import RecipeSummary
|
||||
from mealie.schema.response.pagination import PaginationBase, PaginationQuery, RecipeSearchQuery
|
||||
|
||||
router = APIRouter(prefix="/recipes/{group_slug}")
|
||||
router = APIRouter(prefix="/recipes")
|
||||
|
||||
|
||||
@controller(router)
|
||||
class PublicRecipesController(BasePublicExploreController):
|
||||
class PublicRecipesController(BasePublicHouseholdExploreController):
|
||||
@property
|
||||
def cookbooks(self):
|
||||
return self.repos.cookbooks.by_group(self.group.id)
|
||||
def cross_household_cookbooks(self):
|
||||
return self.cross_household_repos.cookbooks
|
||||
|
||||
@property
|
||||
def recipes(self):
|
||||
return self.repos.recipes.by_group(self.group.id)
|
||||
def cross_household_recipes(self):
|
||||
return self.cross_household_repos.recipes
|
||||
|
||||
@router.get("", response_model=PaginationBase[RecipeSummary])
|
||||
def get_all(
|
||||
@@ -38,7 +39,9 @@ class PublicRecipesController(BasePublicExploreController):
|
||||
foods: list[UUID4 | str] | None = Query(None),
|
||||
) -> PaginationBase[RecipeSummary]:
|
||||
cookbook_data: ReadCookBook | None = None
|
||||
recipes_repo = self.cross_household_recipes
|
||||
if search_query.cookbook:
|
||||
COOKBOOK_NOT_FOUND_EXCEPTION = HTTPException(404, "cookbook not found")
|
||||
if isinstance(search_query.cookbook, UUID):
|
||||
cb_match_attr = "id"
|
||||
else:
|
||||
@@ -47,18 +50,26 @@ class PublicRecipesController(BasePublicExploreController):
|
||||
cb_match_attr = "id"
|
||||
except ValueError:
|
||||
cb_match_attr = "slug"
|
||||
cookbook_data = self.cookbooks.get_one(search_query.cookbook, cb_match_attr)
|
||||
cookbook_data = self.cross_household_cookbooks.get_one(search_query.cookbook, cb_match_attr)
|
||||
|
||||
if cookbook_data is None or not cookbook_data.public:
|
||||
raise HTTPException(status_code=404, detail="cookbook not found")
|
||||
raise COOKBOOK_NOT_FOUND_EXCEPTION
|
||||
household = self.repos.households.get_one(cookbook_data.household_id)
|
||||
if not household or household.preferences.private_household:
|
||||
raise COOKBOOK_NOT_FOUND_EXCEPTION
|
||||
|
||||
public_filter = "settings.public = TRUE"
|
||||
# filter recipes by the cookbook's household
|
||||
recipes_repo = get_repositories(
|
||||
self.session, group_id=self.group_id, household_id=cookbook_data.household_id
|
||||
).recipes
|
||||
|
||||
public_filter = "(household.preferences.privateHousehold = FALSE AND settings.public = TRUE)"
|
||||
if q.query_filter:
|
||||
q.query_filter = f"({q.query_filter}) AND {public_filter}"
|
||||
else:
|
||||
q.query_filter = public_filter
|
||||
|
||||
pagination_response = self.recipes.page_all(
|
||||
pagination_response = recipes_repo.page_all(
|
||||
pagination=q,
|
||||
cookbook=cookbook_data,
|
||||
categories=categories,
|
||||
@@ -75,7 +86,7 @@ class PublicRecipesController(BasePublicExploreController):
|
||||
# merge default pagination with the request's query params
|
||||
query_params = q.model_dump() | {**request.query_params}
|
||||
pagination_response.set_pagination_guides(
|
||||
router.url_path_for("get_all", group_slug=self.group.slug),
|
||||
self.get_explore_url_path(router.url_path_for("get_all")),
|
||||
{k: v for k, v in query_params.items() if v is not None},
|
||||
)
|
||||
|
||||
@@ -86,9 +97,13 @@ class PublicRecipesController(BasePublicExploreController):
|
||||
|
||||
@router.get("/{recipe_slug}", response_model=Recipe)
|
||||
def get_recipe(self, recipe_slug: str) -> Recipe:
|
||||
recipe = self.repos.recipes.by_group(self.group.id).get_one(recipe_slug)
|
||||
RECIPE_NOT_FOUND_EXCEPTION = HTTPException(404, "recipe not found")
|
||||
recipe = self.cross_household_recipes.get_one(recipe_slug)
|
||||
|
||||
if not recipe or not recipe.settings.public:
|
||||
raise HTTPException(404, "recipe not found")
|
||||
raise RECIPE_NOT_FOUND_EXCEPTION
|
||||
household = self.repos.households.get_one(recipe.household_id)
|
||||
if not household or household.preferences.private_household:
|
||||
raise RECIPE_NOT_FOUND_EXCEPTION
|
||||
|
||||
return recipe
|
||||
|
||||
@@ -1,36 +1,17 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import (
|
||||
controller_cookbooks,
|
||||
controller_group_notifications,
|
||||
controller_group_recipe_actions,
|
||||
controller_group_reports,
|
||||
controller_group_self_service,
|
||||
controller_invitations,
|
||||
controller_labels,
|
||||
controller_mealplan,
|
||||
controller_mealplan_config,
|
||||
controller_mealplan_rules,
|
||||
controller_migrations,
|
||||
controller_seeder,
|
||||
controller_shopping_lists,
|
||||
controller_webhooks,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(controller_group_self_service.router)
|
||||
router.include_router(controller_mealplan_rules.router)
|
||||
router.include_router(controller_mealplan_config.router)
|
||||
router.include_router(controller_mealplan.router)
|
||||
router.include_router(controller_cookbooks.router)
|
||||
router.include_router(controller_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_shopping_lists.item_router)
|
||||
router.include_router(controller_labels.router)
|
||||
router.include_router(controller_group_notifications.router)
|
||||
router.include_router(controller_group_recipe_actions.router)
|
||||
router.include_router(controller_seeder.router)
|
||||
|
||||
@@ -17,7 +17,7 @@ router = APIRouter(prefix="/groups/reports", tags=["Groups: Reports"])
|
||||
class GroupReportsController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.group_reports.by_group(self.user.group_id)
|
||||
return self.repos.group_reports
|
||||
|
||||
def registered_exceptions(self, ex: type[Exception]) -> str:
|
||||
return {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from fastapi import Query
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.routes._base.base_controllers 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.group.group_statistics import GroupStatistics, GroupStorage
|
||||
from mealie.schema.user.user import GroupSummary, UserOut
|
||||
from mealie.schema.group.group_statistics import GroupStorage
|
||||
from mealie.schema.household.household import HouseholdSummary
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
from mealie.schema.user.user import GroupSummary, UserSummary
|
||||
from mealie.services.group_services.group_service import GroupService
|
||||
|
||||
router = UserAPIRouter(prefix="/groups", tags=["Groups: Self Service"])
|
||||
@@ -25,10 +27,20 @@ class GroupSelfServiceController(BaseUserController):
|
||||
"""Returns the Group Data for the Current User"""
|
||||
return self.group.cast(GroupSummary)
|
||||
|
||||
@router.get("/members", response_model=list[UserOut])
|
||||
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.get("/members", response_model=list[UserSummary])
|
||||
def get_group_members(self, household_id: UUID4 | None = Query(None, alias="householdId")):
|
||||
"""Returns all users belonging to the current group, optionally filtered by household_id"""
|
||||
|
||||
query_filter = f"household_id={household_id}" if household_id else None
|
||||
private_users = self.repos.users.page_all(PaginationQuery(page=1, per_page=-1, query_filter=query_filter)).items
|
||||
return [user.cast(UserSummary) for user in private_users]
|
||||
|
||||
@router.get("/households", response_model=list[HouseholdSummary])
|
||||
def get_group_households(self):
|
||||
"""Returns all households belonging to the current group"""
|
||||
|
||||
households = self.repos.households.page_all(PaginationQuery(page=1, per_page=-1)).items
|
||||
return [household.cast(HouseholdSummary) for household in households]
|
||||
|
||||
@router.get("/preferences", response_model=ReadGroupPreferences)
|
||||
def get_group_preferences(self):
|
||||
@@ -38,28 +50,6 @@ class GroupSelfServiceController(BaseUserController):
|
||||
def update_group_preferences(self, new_pref: UpdateGroupPreferences):
|
||||
return self.repos.group_preferences.update(self.group_id, new_pref)
|
||||
|
||||
@router.put("/permissions", response_model=UserOut)
|
||||
def set_member_permissions(self, permissions: SetPermissions):
|
||||
self.checks.can_manage()
|
||||
|
||||
target_user = self.repos.users.get_one(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)
|
||||
|
||||
@router.get("/statistics", response_model=GroupStatistics)
|
||||
def get_statistics(self):
|
||||
return self.service.calculate_statistics()
|
||||
|
||||
@router.get("/storage", response_model=GroupStorage)
|
||||
def get_storage(self):
|
||||
return self.service.calculate_group_storage()
|
||||
|
||||
@@ -17,14 +17,14 @@ from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelPagination
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
from mealie.services.group_services.labels_service import MultiPurposeLabelService
|
||||
|
||||
router = APIRouter(prefix="/groups/labels", tags=["Group: Multi Purpose Labels"], route_class=MealieCrudRoute)
|
||||
router = APIRouter(prefix="/groups/labels", tags=["Groups: Multi Purpose Labels"], route_class=MealieCrudRoute)
|
||||
|
||||
|
||||
@controller(router)
|
||||
class MultiPurposeLabelsController(BaseUserController):
|
||||
@cached_property
|
||||
def service(self):
|
||||
return MultiPurposeLabelService(self.repos, self.group.id)
|
||||
return MultiPurposeLabelService(self.repos)
|
||||
|
||||
@cached_property
|
||||
def repo(self):
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
from mealie.routes._base.base_controllers import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import HttpRepo
|
||||
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 HttpRepo[GroupInDB, GroupInDB, GroupInDB](self.repos.groups, self.logger)
|
||||
|
||||
@router.get("", response_model=list[CategoryBase])
|
||||
def get_mealplan_categories(self):
|
||||
data = self.mixins.get_one(self.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.user.group_id)
|
||||
data.categories = new_categories
|
||||
return self.mixins.update_one(data, data.id).categories
|
||||
@@ -21,7 +21,7 @@ from mealie.services.migrations import (
|
||||
TandoorMigrator,
|
||||
)
|
||||
|
||||
router = UserAPIRouter(prefix="/groups/migrations", tags=["Group: Migrations"])
|
||||
router = UserAPIRouter(prefix="/groups/migrations", tags=["Groups: Migrations"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
@@ -43,6 +43,7 @@ class GroupMigrationController(BaseUserController):
|
||||
"db": self.repos,
|
||||
"session": self.session,
|
||||
"user_id": self.user.id,
|
||||
"household_id": self.household_id,
|
||||
"group_id": self.group_id,
|
||||
"add_migration_tag": add_migration_tag,
|
||||
"translator": self.translator,
|
||||
|
||||
@@ -15,7 +15,7 @@ router = APIRouter(prefix="/groups/seeders", tags=["Groups: Seeders"])
|
||||
class DataSeederController(BaseUserController):
|
||||
@cached_property
|
||||
def service(self) -> SeederService:
|
||||
return SeederService(self.repos, self.user, self.group)
|
||||
return SeederService(self.repos)
|
||||
|
||||
def _wrap(self, func):
|
||||
try:
|
||||
|
||||
28
mealie/routes/households/__init__.py
Normal file
28
mealie/routes/households/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import (
|
||||
controller_cookbooks,
|
||||
controller_group_notifications,
|
||||
controller_group_recipe_actions,
|
||||
controller_household_self_service,
|
||||
controller_invitations,
|
||||
controller_mealplan,
|
||||
controller_mealplan_rules,
|
||||
controller_shopping_lists,
|
||||
controller_webhooks,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(controller_cookbooks.router)
|
||||
router.include_router(controller_group_notifications.router)
|
||||
router.include_router(controller_group_recipe_actions.router)
|
||||
router.include_router(controller_household_self_service.router)
|
||||
router.include_router(controller_invitations.router)
|
||||
router.include_router(controller_shopping_lists.router)
|
||||
router.include_router(controller_shopping_lists.item_router)
|
||||
router.include_router(controller_webhooks.router)
|
||||
|
||||
# mealplan_rules must be added before mealplan due to the way the routes are defined
|
||||
router.include_router(controller_mealplan_rules.router)
|
||||
router.include_router(controller_mealplan.router)
|
||||
@@ -1,3 +1,4 @@
|
||||
from collections import defaultdict
|
||||
from functools import cached_property
|
||||
from uuid import UUID
|
||||
|
||||
@@ -19,14 +20,14 @@ from mealie.services.event_bus_service.event_types import (
|
||||
EventTypes,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/groups/cookbooks", tags=["Groups: Cookbooks"], route_class=MealieCrudRoute)
|
||||
router = APIRouter(prefix="/households/cookbooks", tags=["Households: Cookbooks"], route_class=MealieCrudRoute)
|
||||
|
||||
|
||||
@controller(router)
|
||||
class GroupCookbookController(BaseCrudController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.cookbooks.by_group(self.group_id)
|
||||
return self.repos.cookbooks
|
||||
|
||||
def registered_exceptions(self, ex: type[Exception]) -> str:
|
||||
registered = {
|
||||
@@ -54,13 +55,15 @@ class GroupCookbookController(BaseCrudController):
|
||||
|
||||
@router.post("", response_model=ReadCookBook, status_code=201)
|
||||
def create_one(self, data: CreateCookBook):
|
||||
data = mapper.cast(data, SaveCookBook, group_id=self.group_id)
|
||||
data = mapper.cast(data, SaveCookBook, group_id=self.group_id, household_id=self.household_id)
|
||||
cookbook = self.mixins.create_one(data)
|
||||
|
||||
if cookbook:
|
||||
self.publish_event(
|
||||
event_type=EventTypes.cookbook_created,
|
||||
document_data=EventCookbookData(operation=EventOperation.create, cookbook_id=cookbook.id),
|
||||
group_id=cookbook.group_id,
|
||||
household_id=cookbook.household_id,
|
||||
message=self.t("notifications.generic-created", name=cookbook.name),
|
||||
)
|
||||
|
||||
@@ -68,19 +71,25 @@ class GroupCookbookController(BaseCrudController):
|
||||
|
||||
@router.put("", response_model=list[ReadCookBook])
|
||||
def update_many(self, data: list[UpdateCookBook]):
|
||||
updated = []
|
||||
updated_by_group_and_household: defaultdict[UUID4, defaultdict[UUID4, list[ReadCookBook]]] = defaultdict(
|
||||
lambda: defaultdict(list)
|
||||
)
|
||||
|
||||
for cookbook in data:
|
||||
cb = self.mixins.update_one(cookbook, cookbook.id)
|
||||
updated.append(cb)
|
||||
updated_by_group_and_household[cb.group_id][cb.household_id].append(cb)
|
||||
|
||||
if updated:
|
||||
self.publish_event(
|
||||
event_type=EventTypes.cookbook_updated,
|
||||
document_data=EventCookbookBulkData(
|
||||
operation=EventOperation.update, cookbook_ids=[cb.id for cb in updated]
|
||||
),
|
||||
)
|
||||
if updated_by_group_and_household:
|
||||
for group_id, household_dict in updated_by_group_and_household.items():
|
||||
for household_id, updated in household_dict.items():
|
||||
self.publish_event(
|
||||
event_type=EventTypes.cookbook_updated,
|
||||
document_data=EventCookbookBulkData(
|
||||
operation=EventOperation.update, cookbook_ids=[cb.id for cb in updated]
|
||||
),
|
||||
group_id=group_id,
|
||||
household_id=household_id,
|
||||
)
|
||||
|
||||
return updated
|
||||
|
||||
@@ -102,7 +111,7 @@ class GroupCookbookController(BaseCrudController):
|
||||
|
||||
return cookbook.cast(
|
||||
RecipeCookBook,
|
||||
recipes=self.repos.recipes.by_group(self.group_id).by_category_and_tags(
|
||||
recipes=self.repos.recipes.by_category_and_tags(
|
||||
cookbook.categories,
|
||||
cookbook.tags,
|
||||
cookbook.tools,
|
||||
@@ -119,6 +128,8 @@ class GroupCookbookController(BaseCrudController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.cookbook_updated,
|
||||
document_data=EventCookbookData(operation=EventOperation.update, cookbook_id=cookbook.id),
|
||||
group_id=cookbook.group_id,
|
||||
household_id=cookbook.household_id,
|
||||
message=self.t("notifications.generic-updated", name=cookbook.name),
|
||||
)
|
||||
|
||||
@@ -131,6 +142,8 @@ class GroupCookbookController(BaseCrudController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.cookbook_deleted,
|
||||
document_data=EventCookbookData(operation=EventOperation.delete, cookbook_id=cookbook.id),
|
||||
group_id=cookbook.group_id,
|
||||
household_id=cookbook.household_id,
|
||||
message=self.t("notifications.generic-deleted", name=cookbook.name),
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ from mealie.routes._base.base_controllers import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import HttpRepo
|
||||
from mealie.routes._base.routers import MealieCrudRoute
|
||||
from mealie.schema.group.group_events import (
|
||||
from mealie.schema.household.group_events import (
|
||||
GroupEventNotifierCreate,
|
||||
GroupEventNotifierOut,
|
||||
GroupEventNotifierPrivate,
|
||||
@@ -29,7 +29,7 @@ from mealie.services.event_bus_service.event_types import (
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/groups/events/notifications", tags=["Group: Event Notifications"], route_class=MealieCrudRoute
|
||||
prefix="/households/events/notifications", tags=["Households: Event Notifications"], route_class=MealieCrudRoute
|
||||
)
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class GroupEventsNotifierController(BaseUserController):
|
||||
if not self.user:
|
||||
raise Exception("No user is logged in.")
|
||||
|
||||
return self.repos.group_event_notifier.by_group(self.user.group_id)
|
||||
return self.repos.group_event_notifier
|
||||
|
||||
# =======================================================================
|
||||
# CRUD Operations
|
||||
@@ -63,7 +63,7 @@ class GroupEventsNotifierController(BaseUserController):
|
||||
|
||||
@router.post("", response_model=GroupEventNotifierOut, status_code=201)
|
||||
def create_one(self, data: GroupEventNotifierCreate):
|
||||
save_data = cast(data, GroupEventNotifierSave, group_id=self.user.group_id)
|
||||
save_data = cast(data, GroupEventNotifierSave, group_id=self.group_id, household_id=self.household_id)
|
||||
return self.mixins.create_one(save_data)
|
||||
|
||||
@router.get("/{item_id}", response_model=GroupEventNotifierOut)
|
||||
@@ -100,5 +100,5 @@ class GroupEventsNotifierController(BaseUserController):
|
||||
document_data=EventDocumentDataBase(document_type=EventDocumentType.generic, operation=EventOperation.info),
|
||||
)
|
||||
|
||||
test_listener = AppriseEventListener(self.group_id)
|
||||
test_listener = AppriseEventListener(self.group_id, self.household_id)
|
||||
test_listener.publish_to_subscribers(test_event, [item.apprise_url])
|
||||
@@ -6,7 +6,7 @@ from pydantic import UUID4
|
||||
from mealie.routes._base.base_controllers import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import HttpRepo
|
||||
from mealie.schema.group.group_recipe_action import (
|
||||
from mealie.schema.household.group_recipe_action import (
|
||||
CreateGroupRecipeAction,
|
||||
GroupRecipeActionOut,
|
||||
GroupRecipeActionPagination,
|
||||
@@ -14,14 +14,14 @@ from mealie.schema.group.group_recipe_action import (
|
||||
)
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
|
||||
router = APIRouter(prefix="/groups/recipe-actions", tags=["Groups: Recipe Actions"])
|
||||
router = APIRouter(prefix="/households/recipe-actions", tags=["Households: Recipe Actions"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class GroupRecipeActionController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.group_recipe_actions.by_group(self.group_id)
|
||||
return self.repos.group_recipe_actions
|
||||
|
||||
@property
|
||||
def mixins(self):
|
||||
@@ -39,7 +39,7 @@ class GroupRecipeActionController(BaseUserController):
|
||||
|
||||
@router.post("", response_model=GroupRecipeActionOut, status_code=201)
|
||||
def create_one(self, data: CreateGroupRecipeAction):
|
||||
save = data.cast(SaveGroupRecipeAction, group_id=self.group.id)
|
||||
save = data.cast(SaveGroupRecipeAction, group_id=self.group_id, household_id=self.household_id)
|
||||
return self.mixins.create_one(save)
|
||||
|
||||
@router.get("/{item_id}", response_model=GroupRecipeActionOut)
|
||||
@@ -0,0 +1,69 @@
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from mealie.routes._base.base_controllers import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.schema.household.household import HouseholdInDB
|
||||
from mealie.schema.household.household_permissions import SetPermissions
|
||||
from mealie.schema.household.household_preferences import ReadHouseholdPreferences, UpdateHouseholdPreferences
|
||||
from mealie.schema.household.household_statistics import HouseholdStatistics
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
from mealie.schema.user.user import UserOut
|
||||
from mealie.services.household_services.household_service import HouseholdService
|
||||
|
||||
router = UserAPIRouter(prefix="/households", tags=["Households: Self Service"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class HouseholdSelfServiceController(BaseUserController):
|
||||
@cached_property
|
||||
def service(self) -> HouseholdService:
|
||||
return HouseholdService(self.group_id, self.household_id, self.repos)
|
||||
|
||||
@router.get("/self", response_model=HouseholdInDB)
|
||||
def get_logged_in_user_household(self):
|
||||
"""Returns the Household Data for the Current User"""
|
||||
return self.household
|
||||
|
||||
@router.get("/members", response_model=list[UserOut])
|
||||
def get_household_members(self):
|
||||
"""Returns all users belonging to the current household"""
|
||||
private_users = self.repos.users.page_all(
|
||||
PaginationQuery(page=1, per_page=-1, query_filter=f"household_id={self.household_id}")
|
||||
).items
|
||||
return [user.cast(UserOut) for user in private_users]
|
||||
|
||||
@router.get("/preferences", response_model=ReadHouseholdPreferences)
|
||||
def get_household_preferences(self):
|
||||
return self.household.preferences
|
||||
|
||||
@router.put("/preferences", response_model=ReadHouseholdPreferences)
|
||||
def update_household_preferences(self, new_pref: UpdateHouseholdPreferences):
|
||||
return self.repos.household_preferences.update(self.household_id, new_pref)
|
||||
|
||||
@router.put("/permissions", response_model=UserOut)
|
||||
def set_member_permissions(self, permissions: SetPermissions):
|
||||
self.checks.can_manage()
|
||||
|
||||
target_user = self.repos.users.get_one(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")
|
||||
|
||||
if target_user.household_id != self.household_id:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="User is not a member of this household")
|
||||
|
||||
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)
|
||||
|
||||
@router.get("/statistics", response_model=HouseholdStatistics)
|
||||
def get_statistics(self):
|
||||
return self.service.calculate_statistics()
|
||||
@@ -4,23 +4,24 @@ from fastapi import APIRouter, Header, HTTPException, status
|
||||
|
||||
from mealie.core.security import url_safe_token
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.schema.group.invite_token import (
|
||||
from mealie.schema.household.invite_token import (
|
||||
CreateInviteToken,
|
||||
EmailInitationResponse,
|
||||
EmailInvitation,
|
||||
ReadInviteToken,
|
||||
SaveInviteToken,
|
||||
)
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
from mealie.services.email.email_service import EmailService
|
||||
|
||||
router = APIRouter(prefix="/groups/invitations", tags=["Groups: Invitations"])
|
||||
router = APIRouter(prefix="/households/invitations", tags=["Households: 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})
|
||||
return self.repos.group_invite_tokens.page_all(PaginationQuery(page=1, per_page=-1)).items
|
||||
|
||||
@router.post("", response_model=ReadInviteToken, status_code=status.HTTP_201_CREATED)
|
||||
def create_invite_token(self, uses: CreateInviteToken):
|
||||
@@ -30,7 +31,9 @@ class GroupInvitationsController(BaseUserController):
|
||||
detail="User is not allowed to create invite tokens",
|
||||
)
|
||||
|
||||
token = SaveInviteToken(uses_left=uses.uses, group_id=self.group_id, token=url_safe_token())
|
||||
token = SaveInviteToken(
|
||||
uses_left=uses.uses, group_id=self.group_id, household_id=self.household_id, token=url_safe_token()
|
||||
)
|
||||
return self.repos.group_invite_tokens.create(token)
|
||||
|
||||
@router.post("/email", response_model=EmailInitationResponse)
|
||||
@@ -17,14 +17,14 @@ from mealie.schema.response.pagination import PaginationQuery
|
||||
from mealie.schema.response.responses import ErrorResponse
|
||||
from mealie.services.event_bus_service.event_types import EventMealplanCreatedData, EventTypes
|
||||
|
||||
router = APIRouter(prefix="/groups/mealplans", tags=["Groups: Mealplans"])
|
||||
router = APIRouter(prefix="/households/mealplans", tags=["Households: Mealplans"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class GroupMealplanController(BaseCrudController):
|
||||
@cached_property
|
||||
def repo(self) -> RepositoryMeals:
|
||||
return self.repos.meals.by_group(self.group_id)
|
||||
return self.repos.meals
|
||||
|
||||
def registered_exceptions(self, ex: type[Exception]) -> str:
|
||||
registered = {
|
||||
@@ -42,7 +42,7 @@ class GroupMealplanController(BaseCrudController):
|
||||
|
||||
@router.get("/today")
|
||||
def get_todays_meals(self):
|
||||
return self.repo.get_today(group_id=self.group_id)
|
||||
return self.repo.get_today()
|
||||
|
||||
@router.post("/random", response_model=ReadPlanEntry)
|
||||
def create_random_meal(self, data: CreateRandomEntry):
|
||||
@@ -55,11 +55,9 @@ class GroupMealplanController(BaseCrudController):
|
||||
to the random meal selector.
|
||||
"""
|
||||
# Get relevant group rules
|
||||
rules = self.repos.group_meal_plan_rules.by_group(self.group_id).get_rules(
|
||||
PlanRulesDay.from_date(data.date), data.entry_type.value
|
||||
)
|
||||
rules = self.repos.group_meal_plan_rules.get_rules(PlanRulesDay.from_date(data.date), data.entry_type.value)
|
||||
|
||||
recipe_repo = self.repos.recipes.by_group(self.group_id)
|
||||
recipe_repo = self.repos.recipes
|
||||
random_recipes: list[Recipe] = []
|
||||
|
||||
if not rules: # If no rules are set, return any random recipe from the group
|
||||
@@ -74,9 +72,7 @@ class GroupMealplanController(BaseCrudController):
|
||||
categories.extend(rule.categories)
|
||||
|
||||
if tags or categories:
|
||||
random_recipes = self.repos.recipes.by_group(self.group_id).get_random_by_categories_and_tags(
|
||||
categories, tags
|
||||
)
|
||||
random_recipes = self.repos.recipes.get_random_by_categories_and_tags(categories, tags)
|
||||
else:
|
||||
random_recipes = recipe_repo.get_random()
|
||||
|
||||
@@ -124,7 +120,7 @@ class GroupMealplanController(BaseCrudController):
|
||||
|
||||
@router.post("", response_model=ReadPlanEntry, status_code=201)
|
||||
def create_one(self, data: CreatePlanEntry):
|
||||
data = mapper.cast(data, SavePlanEntry, group_id=self.group.id, user_id=self.user.id)
|
||||
data = mapper.cast(data, SavePlanEntry, group_id=self.group_id, user_id=self.user.id)
|
||||
result = self.mixins.create_one(data)
|
||||
|
||||
self.publish_event(
|
||||
@@ -136,6 +132,8 @@ class GroupMealplanController(BaseCrudController):
|
||||
recipe_slug=result.recipe.slug if result.recipe else None,
|
||||
date=data.date,
|
||||
),
|
||||
group_id=result.group_id,
|
||||
household_id=result.household_id,
|
||||
message=f"Mealplan entry created for {data.date} for {data.entry_type}",
|
||||
)
|
||||
|
||||
@@ -11,14 +11,14 @@ from mealie.schema import mapper
|
||||
from mealie.schema.meal_plan.plan_rules import PlanRulesCreate, PlanRulesOut, PlanRulesPagination, PlanRulesSave
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
|
||||
router = UserAPIRouter(prefix="/groups/mealplans/rules", tags=["Groups: Mealplan Rules"])
|
||||
router = UserAPIRouter(prefix="/households/mealplans/rules", tags=["Households: Mealplan Rules"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class GroupMealplanConfigController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.group_meal_plan_rules.by_group(self.group_id)
|
||||
return self.repos.group_meal_plan_rules
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
@@ -36,7 +36,7 @@ class GroupMealplanConfigController(BaseUserController):
|
||||
|
||||
@router.post("", response_model=PlanRulesOut, status_code=201)
|
||||
def create_one(self, data: PlanRulesCreate):
|
||||
save = mapper.cast(data, PlanRulesSave, group_id=self.group.id)
|
||||
save = mapper.cast(data, PlanRulesSave, group_id=self.group.id, household_id=self.household.id)
|
||||
return self.mixins.create_one(save)
|
||||
|
||||
@router.get("/{item_id}", response_model=PlanRulesOut)
|
||||
@@ -7,7 +7,7 @@ from pydantic import UUID4
|
||||
from mealie.routes._base.base_controllers import BaseCrudController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import HttpRepo
|
||||
from mealie.schema.group.group_shopping_list import (
|
||||
from mealie.schema.household.group_shopping_list import (
|
||||
ShoppingListAddRecipeParams,
|
||||
ShoppingListCreate,
|
||||
ShoppingListItemCreate,
|
||||
@@ -32,9 +32,9 @@ from mealie.services.event_bus_service.event_types import (
|
||||
EventShoppingListItemBulkData,
|
||||
EventTypes,
|
||||
)
|
||||
from mealie.services.group_services.shopping_lists import ShoppingListService
|
||||
from mealie.services.household_services.shopping_lists import ShoppingListService
|
||||
|
||||
item_router = APIRouter(prefix="/groups/shopping/items", tags=["Group: Shopping List Items"])
|
||||
item_router = APIRouter(prefix="/households/shopping/items", tags=["Households: Shopping List Items"])
|
||||
|
||||
|
||||
def publish_list_item_events(publisher: Callable, items_collection: ShoppingListItemsCollectionOut) -> None:
|
||||
@@ -52,6 +52,9 @@ def publish_list_item_events(publisher: Callable, items_collection: ShoppingList
|
||||
shopping_list_id=shopping_list_id,
|
||||
shopping_list_item_ids=[item.id for item in items],
|
||||
),
|
||||
# since these are all the same shopping list, they share a group_id and household_id
|
||||
group_id=items[0].group_id,
|
||||
household_id=items[0].household_id,
|
||||
)
|
||||
|
||||
if items_collection.updated_items:
|
||||
@@ -67,6 +70,9 @@ def publish_list_item_events(publisher: Callable, items_collection: ShoppingList
|
||||
shopping_list_id=shopping_list_id,
|
||||
shopping_list_item_ids=[item.id for item in items],
|
||||
),
|
||||
# since these are all the same shopping list, they share a group_id and household_id
|
||||
group_id=items[0].group_id,
|
||||
household_id=items[0].household_id,
|
||||
)
|
||||
|
||||
if items_collection.deleted_items:
|
||||
@@ -82,6 +88,9 @@ def publish_list_item_events(publisher: Callable, items_collection: ShoppingList
|
||||
shopping_list_id=shopping_list_id,
|
||||
shopping_list_item_ids=[item.id for item in items],
|
||||
),
|
||||
# since these are all the same shopping list, they share a group_id and household_id
|
||||
group_id=items[0].group_id,
|
||||
household_id=items[0].household_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -89,7 +98,7 @@ def publish_list_item_events(publisher: Callable, items_collection: ShoppingList
|
||||
class ShoppingListItemController(BaseCrudController):
|
||||
@cached_property
|
||||
def service(self):
|
||||
return ShoppingListService(self.repos, self.group, self.user)
|
||||
return ShoppingListService(self.repos)
|
||||
|
||||
@cached_property
|
||||
def repo(self):
|
||||
@@ -143,18 +152,18 @@ class ShoppingListItemController(BaseCrudController):
|
||||
return self.delete_many([item_id])
|
||||
|
||||
|
||||
router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists"])
|
||||
router = APIRouter(prefix="/households/shopping/lists", tags=["Households: Shopping Lists"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class ShoppingListController(BaseCrudController):
|
||||
@cached_property
|
||||
def service(self):
|
||||
return ShoppingListService(self.repos, self.group, self.user)
|
||||
return ShoppingListService(self.repos)
|
||||
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.group_shopping_lists.by_group(self.user.group_id)
|
||||
return self.repos.group_shopping_lists
|
||||
|
||||
# =======================================================================
|
||||
# CRUD Operations
|
||||
@@ -175,11 +184,13 @@ class ShoppingListController(BaseCrudController):
|
||||
|
||||
@router.post("", response_model=ShoppingListOut, status_code=201)
|
||||
def create_one(self, data: ShoppingListCreate):
|
||||
shopping_list = self.service.create_one_list(data)
|
||||
shopping_list = self.service.create_one_list(data, self.user.id)
|
||||
if shopping_list:
|
||||
self.publish_event(
|
||||
event_type=EventTypes.shopping_list_created,
|
||||
document_data=EventShoppingListData(operation=EventOperation.create, shopping_list_id=shopping_list.id),
|
||||
group_id=shopping_list.group_id,
|
||||
household_id=shopping_list.household_id,
|
||||
message=self.t("notifications.generic-created", name=shopping_list.name),
|
||||
)
|
||||
|
||||
@@ -195,6 +206,8 @@ class ShoppingListController(BaseCrudController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.shopping_list_updated,
|
||||
document_data=EventShoppingListData(operation=EventOperation.update, shopping_list_id=shopping_list.id),
|
||||
group_id=shopping_list.group_id,
|
||||
household_id=shopping_list.household_id,
|
||||
message=self.t("notifications.generic-updated", name=shopping_list.name),
|
||||
)
|
||||
|
||||
@@ -207,6 +220,8 @@ class ShoppingListController(BaseCrudController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.shopping_list_deleted,
|
||||
document_data=EventShoppingListData(operation=EventOperation.delete, shopping_list_id=shopping_list.id),
|
||||
group_id=shopping_list.group_id,
|
||||
household_id=shopping_list.household_id,
|
||||
message=self.t("notifications.generic-deleted", name=shopping_list.name),
|
||||
)
|
||||
|
||||
@@ -252,6 +267,8 @@ class ShoppingListController(BaseCrudController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.shopping_list_updated,
|
||||
document_data=EventShoppingListData(operation=EventOperation.update, shopping_list_id=updated_list.id),
|
||||
group_id=updated_list.group_id,
|
||||
household_id=updated_list.household_id,
|
||||
message=self.t("notifications.generic-updated", name=updated_list.name),
|
||||
)
|
||||
|
||||
@@ -8,18 +8,18 @@ from mealie.routes._base.base_controllers import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import HttpRepo
|
||||
from mealie.schema import mapper
|
||||
from mealie.schema.group.webhook import CreateWebhook, ReadWebhook, SaveWebhook, WebhookPagination
|
||||
from mealie.schema.household.webhook import CreateWebhook, ReadWebhook, SaveWebhook, WebhookPagination
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
from mealie.services.scheduler.tasks.post_webhooks import post_group_webhooks, post_single_webhook
|
||||
|
||||
router = APIRouter(prefix="/groups/webhooks", tags=["Groups: Webhooks"])
|
||||
router = APIRouter(prefix="/households/webhooks", tags=["Households: Webhooks"])
|
||||
|
||||
|
||||
@controller(router)
|
||||
class ReadWebhookController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.webhooks.by_group(self.group_id)
|
||||
return self.repos.webhooks
|
||||
|
||||
@property
|
||||
def mixins(self) -> HttpRepo:
|
||||
@@ -37,7 +37,7 @@ class ReadWebhookController(BaseUserController):
|
||||
|
||||
@router.post("", response_model=ReadWebhook, status_code=201)
|
||||
def create_one(self, data: CreateWebhook):
|
||||
save = mapper.cast(data, SaveWebhook, group_id=self.group.id)
|
||||
save = mapper.cast(data, SaveWebhook, group_id=self.group_id, household_id=self.household_id)
|
||||
return self.mixins.create_one(save)
|
||||
|
||||
@router.post("/rerun")
|
||||
@@ -46,7 +46,7 @@ class ReadWebhookController(BaseUserController):
|
||||
|
||||
start_time = datetime.min.time()
|
||||
start_dt = datetime.combine(datetime.now(timezone.utc).date(), start_time)
|
||||
post_group_webhooks(start_dt=start_dt, group_id=self.group.id)
|
||||
post_group_webhooks(start_dt=start_dt, group_id=self.group.id, household_id=self.household.id)
|
||||
|
||||
@router.get("/{item_id}", response_model=ReadWebhook)
|
||||
def get_one(self, item_id: UUID4):
|
||||
@@ -12,7 +12,7 @@ These routes are for development only! These assets are served by Caddy when not
|
||||
in development mode. If you make changes, be sure to test the production container.
|
||||
"""
|
||||
|
||||
router = APIRouter(prefix="/recipes")
|
||||
router = APIRouter(prefix="/recipes", include_in_schema=False)
|
||||
|
||||
|
||||
class ImageType(str, Enum):
|
||||
|
||||
@@ -9,7 +9,7 @@ These routes are for development only! These assets are served by Caddy when not
|
||||
in development mode. If you make changes, be sure to test the production container.
|
||||
"""
|
||||
|
||||
router = APIRouter(prefix="/users")
|
||||
router = APIRouter(prefix="/users", include_in_schema=False)
|
||||
|
||||
|
||||
@router.get("/{user_id}/{file_name}", response_class=FileResponse)
|
||||
|
||||
@@ -8,7 +8,7 @@ from mealie.routes._base.mixins import HttpRepo
|
||||
from mealie.schema import mapper
|
||||
from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse
|
||||
from mealie.schema.recipe.recipe import RecipeCategory, RecipeCategoryPagination
|
||||
from mealie.schema.recipe.recipe_category import CategoryBase, CategorySave
|
||||
from mealie.schema.recipe.recipe_category import CategoryBase, CategoryOut, CategorySave
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
from mealie.services import urls
|
||||
from mealie.services.event_bus_service.event_types import EventCategoryData, EventOperation, EventTypes
|
||||
@@ -29,11 +29,11 @@ class RecipeCategoryController(BaseCrudController):
|
||||
# CRUD Operations
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.categories.by_group(self.group_id)
|
||||
return self.repos.categories
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return HttpRepo(self.repo, self.logger)
|
||||
return HttpRepo[CategorySave, CategoryOut, CategorySave](self.repo, self.logger)
|
||||
|
||||
@router.get("", response_model=RecipeCategoryPagination)
|
||||
def get_all(self, q: PaginationQuery = Depends(PaginationQuery), search: str | None = None):
|
||||
@@ -56,6 +56,8 @@ class RecipeCategoryController(BaseCrudController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.category_created,
|
||||
document_data=EventCategoryData(operation=EventOperation.create, category_id=new_category.id),
|
||||
group_id=new_category.group_id,
|
||||
household_id=None,
|
||||
message=self.t(
|
||||
"notifications.generic-created-with-url",
|
||||
name=new_category.name,
|
||||
@@ -82,6 +84,8 @@ class RecipeCategoryController(BaseCrudController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.category_updated,
|
||||
document_data=EventCategoryData(operation=EventOperation.update, category_id=category.id),
|
||||
group_id=category.group_id,
|
||||
household_id=None,
|
||||
message=self.t(
|
||||
"notifications.generic-updated-with-url",
|
||||
name=category.name,
|
||||
@@ -102,6 +106,8 @@ class RecipeCategoryController(BaseCrudController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.category_deleted,
|
||||
document_data=EventCategoryData(operation=EventOperation.delete, category_id=category.id),
|
||||
group_id=category.group_id,
|
||||
household_id=None,
|
||||
message=self.t("notifications.generic-deleted", name=category.name),
|
||||
)
|
||||
|
||||
@@ -121,5 +127,5 @@ class RecipeCategoryController(BaseCrudController):
|
||||
id=category.id,
|
||||
slug=category.slug,
|
||||
name=category.name,
|
||||
recipes=self.repos.recipes.by_group(self.group_id).get_by_categories([category]),
|
||||
recipes=self.repos.recipes.get_by_categories([category]),
|
||||
)
|
||||
|
||||
@@ -20,7 +20,7 @@ router = APIRouter(prefix="/tags", tags=["Organizer: Tags"])
|
||||
class TagController(BaseCrudController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.tags.by_group(self.group_id)
|
||||
return self.repos.tags
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
@@ -58,6 +58,8 @@ class TagController(BaseCrudController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.tag_created,
|
||||
document_data=EventTagData(operation=EventOperation.create, tag_id=new_tag.id),
|
||||
group_id=new_tag.group_id,
|
||||
household_id=None,
|
||||
message=self.t(
|
||||
"notifications.generic-created-with-url",
|
||||
name=new_tag.name,
|
||||
@@ -77,6 +79,8 @@ class TagController(BaseCrudController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.tag_updated,
|
||||
document_data=EventTagData(operation=EventOperation.update, tag_id=tag.id),
|
||||
group_id=tag.group_id,
|
||||
household_id=None,
|
||||
message=self.t(
|
||||
"notifications.generic-updated-with-url",
|
||||
name=tag.name,
|
||||
@@ -103,6 +107,8 @@ class TagController(BaseCrudController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.tag_deleted,
|
||||
document_data=EventTagData(operation=EventOperation.delete, tag_id=tag.id),
|
||||
group_id=tag.group_id,
|
||||
household_id=None,
|
||||
message=self.t("notifications.generic-deleted", name=tag.name),
|
||||
)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ router = APIRouter(prefix="/tools", tags=["Organizer: Tools"])
|
||||
class RecipeToolController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.tools.by_group(self.group_id)
|
||||
return self.repos.tools
|
||||
|
||||
@property
|
||||
def mixins(self) -> HttpRepo:
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import all_recipe_routes, bulk_actions, comments, recipe_crud_routes, shared_routes, timeline_events
|
||||
from . import bulk_actions, comments, recipe_crud_routes, shared_routes, timeline_events
|
||||
|
||||
prefix = "/recipes"
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(all_recipe_routes.router, prefix=prefix, tags=["Recipe: Query All"])
|
||||
router.include_router(recipe_crud_routes.router_exports)
|
||||
router.include_router(recipe_crud_routes.router)
|
||||
router.include_router(comments.router, prefix=prefix, tags=["Recipe: Comments"])
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
from fastapi import APIRouter, 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.schema.recipe import RecipeSummary
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/summary/untagged", response_model=list[RecipeSummary])
|
||||
async def get_untagged_recipes(count: bool = False, session: Session = Depends(generate_session)):
|
||||
db = get_repositories(session)
|
||||
return db.recipes.count_untagged(count=count, override_schema=RecipeSummary)
|
||||
|
||||
|
||||
@router.get("/summary/uncategorized", response_model=list[RecipeSummary])
|
||||
async def get_uncategorized_recipes(count: bool = False, session: Session = Depends(generate_session)):
|
||||
db = get_repositories(session)
|
||||
return db.recipes.count_uncategorized(count=count, override_schema=RecipeSummary)
|
||||
@@ -10,5 +10,5 @@ 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)
|
||||
recipe = self.repos.recipes.get_one(slug)
|
||||
return self.repos.comments.multi_query({"recipe_id": recipe.id})
|
||||
|
||||
@@ -30,7 +30,7 @@ from mealie.core.dependencies import (
|
||||
validate_recipe_token,
|
||||
)
|
||||
from mealie.core.security import create_recipe_slug_token
|
||||
from mealie.db.models.group.cookbook import CookBook
|
||||
from mealie.db.models.household.cookbook import CookBook
|
||||
from mealie.pkgs import cache
|
||||
from mealie.repos.repository_generic import RepositoryGeneric
|
||||
from mealie.repos.repository_recipes import RepositoryRecipes
|
||||
@@ -95,15 +95,15 @@ class JSONBytes(JSONResponse):
|
||||
class BaseRecipeController(BaseCrudController):
|
||||
@cached_property
|
||||
def repo(self) -> RepositoryRecipes:
|
||||
return self.repos.recipes.by_group(self.group_id)
|
||||
return self.repos.recipes
|
||||
|
||||
@cached_property
|
||||
def cookbooks_repo(self) -> RepositoryGeneric[ReadCookBook, CookBook]:
|
||||
return self.repos.cookbooks.by_group(self.group_id)
|
||||
return self.repos.cookbooks
|
||||
|
||||
@cached_property
|
||||
def service(self) -> RecipeService:
|
||||
return RecipeService(self.repos, self.user, self.group, translator=self.translator)
|
||||
return RecipeService(self.repos, self.user, self.household, translator=self.translator)
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
@@ -207,7 +207,7 @@ class RecipeController(BaseRecipeController):
|
||||
) from e
|
||||
|
||||
if req.include_tags:
|
||||
ctx = ScraperContext(self.user.id, self.group_id, self.repos)
|
||||
ctx = ScraperContext(self.repos)
|
||||
|
||||
recipe.tags = extras.use_tags(ctx) # type: ignore
|
||||
|
||||
@@ -217,6 +217,8 @@ class RecipeController(BaseRecipeController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.recipe_created,
|
||||
document_data=EventRecipeData(operation=EventOperation.create, recipe_slug=new_recipe.slug),
|
||||
group_id=new_recipe.group_id,
|
||||
household_id=new_recipe.household_id,
|
||||
message=self.t(
|
||||
"notifications.generic-created-with-url",
|
||||
name=new_recipe.name,
|
||||
@@ -236,6 +238,8 @@ class RecipeController(BaseRecipeController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.recipe_created,
|
||||
document_data=EventRecipeBulkReportData(operation=EventOperation.create, report_id=report_id),
|
||||
group_id=self.group_id,
|
||||
household_id=self.household_id,
|
||||
)
|
||||
|
||||
return {"reportId": report_id}
|
||||
@@ -265,6 +269,8 @@ class RecipeController(BaseRecipeController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.recipe_created,
|
||||
document_data=EventRecipeData(operation=EventOperation.create, recipe_slug=recipe.slug),
|
||||
group_id=recipe.group_id,
|
||||
household_id=recipe.household_id,
|
||||
)
|
||||
|
||||
return recipe.slug
|
||||
@@ -290,6 +296,8 @@ class RecipeController(BaseRecipeController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.recipe_created,
|
||||
document_data=EventRecipeData(operation=EventOperation.create, recipe_slug=recipe.slug),
|
||||
group_id=recipe.group_id,
|
||||
household_id=recipe.household_id,
|
||||
)
|
||||
|
||||
return recipe.slug
|
||||
@@ -324,7 +332,7 @@ class RecipeController(BaseRecipeController):
|
||||
raise HTTPException(status_code=404, detail="cookbook not found")
|
||||
|
||||
# we use the repo by user so we can sort favorites correctly
|
||||
pagination_response = self.repo.by_user(self.user.id).page_all(
|
||||
pagination_response = self.repos.recipes.by_user(self.user.id).page_all(
|
||||
pagination=q,
|
||||
cookbook=cookbook_data,
|
||||
categories=categories,
|
||||
@@ -374,6 +382,8 @@ class RecipeController(BaseRecipeController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.recipe_created,
|
||||
document_data=EventRecipeData(operation=EventOperation.create, recipe_slug=new_recipe.slug),
|
||||
group_id=new_recipe.group_id,
|
||||
household_id=new_recipe.household_id,
|
||||
message=self.t(
|
||||
"notifications.generic-created-with-url",
|
||||
name=new_recipe.name,
|
||||
@@ -395,6 +405,8 @@ class RecipeController(BaseRecipeController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.recipe_created,
|
||||
document_data=EventRecipeData(operation=EventOperation.create, recipe_slug=new_recipe.slug),
|
||||
group_id=new_recipe.group_id,
|
||||
household_id=new_recipe.household_id,
|
||||
message=self.t(
|
||||
"notifications.generic-duplicated",
|
||||
name=new_recipe.name,
|
||||
@@ -415,6 +427,8 @@ class RecipeController(BaseRecipeController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.recipe_updated,
|
||||
document_data=EventRecipeData(operation=EventOperation.update, recipe_slug=recipe.slug),
|
||||
group_id=recipe.group_id,
|
||||
household_id=recipe.household_id,
|
||||
message=self.t(
|
||||
"notifications.generic-updated-with-url",
|
||||
name=recipe.name,
|
||||
@@ -436,6 +450,8 @@ class RecipeController(BaseRecipeController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.recipe_updated,
|
||||
document_data=EventRecipeData(operation=EventOperation.update, recipe_slug=recipe.slug),
|
||||
group_id=recipe.group_id,
|
||||
household_id=recipe.household_id,
|
||||
message=self.t(
|
||||
"notifications.generic-updated-with-url",
|
||||
name=recipe.name,
|
||||
@@ -458,6 +474,8 @@ class RecipeController(BaseRecipeController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.recipe_updated,
|
||||
document_data=EventRecipeData(operation=EventOperation.update, recipe_slug=recipe.slug),
|
||||
group_id=recipe.group_id,
|
||||
household_id=recipe.household_id,
|
||||
message=self.t(
|
||||
"notifications.generic-updated-with-url",
|
||||
name=recipe.name,
|
||||
@@ -479,6 +497,8 @@ class RecipeController(BaseRecipeController):
|
||||
self.publish_event(
|
||||
event_type=EventTypes.recipe_deleted,
|
||||
document_data=EventRecipeData(operation=EventOperation.delete, recipe_slug=recipe.slug),
|
||||
group_id=recipe.group_id,
|
||||
household_id=recipe.household_id,
|
||||
message=self.t("notifications.generic-deleted", name=recipe.name),
|
||||
)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ router = APIRouter()
|
||||
|
||||
@router.get("/shared/{token_id}", response_model=Recipe)
|
||||
def get_shared_recipe(token_id: UUID4, session: Session = Depends(generate_session)):
|
||||
db = get_repositories(session)
|
||||
db = get_repositories(session, group_id=None, household_id=None)
|
||||
|
||||
token_summary = db.recipe_share_tokens.get_one(token_id)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class RecipeTimelineEventsController(BaseCrudController):
|
||||
|
||||
@cached_property
|
||||
def recipes_repo(self):
|
||||
return self.repos.recipes.by_group(self.group_id)
|
||||
return self.repos.recipes
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
@@ -69,6 +69,8 @@ class RecipeTimelineEventsController(BaseCrudController):
|
||||
document_data=EventRecipeTimelineEventData(
|
||||
operation=EventOperation.create, recipe_slug=recipe.slug, recipe_timeline_event_id=event.id
|
||||
),
|
||||
group_id=recipe.group_id,
|
||||
household_id=recipe.household_id,
|
||||
message=self.t(
|
||||
"notifications.generic-updated-with-url",
|
||||
name=recipe.name,
|
||||
@@ -92,6 +94,8 @@ class RecipeTimelineEventsController(BaseCrudController):
|
||||
document_data=EventRecipeTimelineEventData(
|
||||
operation=EventOperation.update, recipe_slug=recipe.slug, recipe_timeline_event_id=event.id
|
||||
),
|
||||
group_id=recipe.group_id,
|
||||
household_id=recipe.household_id,
|
||||
message=self.t(
|
||||
"notifications.generic-updated-with-url",
|
||||
name=recipe.name,
|
||||
@@ -117,6 +121,8 @@ class RecipeTimelineEventsController(BaseCrudController):
|
||||
document_data=EventRecipeTimelineEventData(
|
||||
operation=EventOperation.delete, recipe_slug=recipe.slug, recipe_timeline_event_id=event.id
|
||||
),
|
||||
group_id=recipe.group_id,
|
||||
household_id=recipe.household_id,
|
||||
message=self.t(
|
||||
"notifications.generic-updated-with-url",
|
||||
name=recipe.name,
|
||||
@@ -145,6 +151,8 @@ class RecipeTimelineEventsController(BaseCrudController):
|
||||
document_data=EventRecipeTimelineEventData(
|
||||
operation=EventOperation.update, recipe_slug=recipe.slug, recipe_timeline_event_id=event.id
|
||||
),
|
||||
group_id=recipe.group_id,
|
||||
household_id=recipe.household_id,
|
||||
message=self.t(
|
||||
"notifications.generic-updated-with-url",
|
||||
name=recipe.name,
|
||||
|
||||
@@ -15,7 +15,7 @@ router = UserAPIRouter(prefix="/shared/recipes", tags=["Shared: Recipes"])
|
||||
class RecipeSharedController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.recipe_share_tokens.by_group(self.group_id)
|
||||
return self.repos.recipe_share_tokens
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
|
||||
@@ -162,13 +162,14 @@ def serve_recipe_with_meta_public(
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
try:
|
||||
repos = AllRepositories(session)
|
||||
group = repos.groups.get_by_slug_or_id(group_slug)
|
||||
public_repos = AllRepositories(session)
|
||||
group = public_repos.groups.get_by_slug_or_id(group_slug)
|
||||
|
||||
if not group or group.preferences.private_group: # type: ignore
|
||||
return response_404()
|
||||
|
||||
recipe = repos.recipes.by_group(group.id).get_one(recipe_slug)
|
||||
group_repos = AllRepositories(session, group_id=group.id)
|
||||
recipe = group_repos.recipes.get_one(recipe_slug)
|
||||
|
||||
if not recipe or not recipe.settings.public: # type: ignore
|
||||
return response_404()
|
||||
@@ -189,9 +190,9 @@ async def serve_recipe_with_meta(
|
||||
return serve_recipe_with_meta_public(group_slug, recipe_slug, session)
|
||||
|
||||
try:
|
||||
repos = AllRepositories(session)
|
||||
repos = AllRepositories(session, group_id=user.group_id)
|
||||
|
||||
recipe = repos.recipes.by_group(user.group_id).get_one(recipe_slug, "slug")
|
||||
recipe = repos.recipes.get_one(recipe_slug, "slug")
|
||||
if recipe is None:
|
||||
return response_404()
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ router = APIRouter(prefix="/foods", tags=["Recipes: Foods"], route_class=MealieC
|
||||
class IngredientFoodsController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.ingredient_foods.by_group(self.group_id)
|
||||
return self.repos.ingredient_foods
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
|
||||
@@ -25,7 +25,7 @@ router = APIRouter(prefix="/units", tags=["Recipes: Units"], route_class=MealieC
|
||||
class IngredientUnitsController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.repos.ingredient_units.by_group(self.group_id)
|
||||
return self.repos.ingredient_units
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
|
||||
@@ -11,7 +11,7 @@ from mealie.routes.users._helpers import assert_user_change_allowed
|
||||
from mealie.schema.response import ErrorResponse, SuccessResponse
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserOut
|
||||
from mealie.schema.user.user import UserPagination, UserRatings, UserRatingSummary, UserSummary, UserSummaryPagination
|
||||
from mealie.schema.user.user import UserPagination, UserRatings, UserRatingSummary
|
||||
|
||||
user_router = UserAPIRouter(prefix="/users", tags=["Users: CRUD"])
|
||||
admin_router = AdminAPIRouter(prefix="/users", tags=["Users: Admin CRUD"])
|
||||
@@ -58,18 +58,6 @@ class AdminUserController(BaseAdminController):
|
||||
|
||||
@controller(user_router)
|
||||
class UserController(BaseUserController):
|
||||
@user_router.get("/group-users", response_model=UserSummaryPagination)
|
||||
def get_all_group_users(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||
"""Returns all users from the current group"""
|
||||
|
||||
response = self.repos.users.by_group(self.group_id).page_all(
|
||||
pagination=q,
|
||||
override=UserSummary,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(user_router.url_path_for("get_all_group_users"), q.model_dump())
|
||||
return response
|
||||
|
||||
@user_router.get("/self", response_model=UserOut)
|
||||
def get_logged_in_user(self):
|
||||
return self.user
|
||||
|
||||
@@ -22,7 +22,7 @@ class UserRatingsController(BaseUserController):
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
recipes_repo = self.repos.recipes.by_group(self.group_id)
|
||||
recipes_repo = self.repos.recipes
|
||||
if isinstance(slug_or_id, UUID):
|
||||
recipe = recipes_repo.get_one(slug_or_id, key="id")
|
||||
else:
|
||||
|
||||
@@ -29,7 +29,7 @@ class RegistrationController(BasePublicController):
|
||||
|
||||
registration_service = RegistrationService(
|
||||
self.logger,
|
||||
get_repositories(self.session),
|
||||
get_repositories(self.session, group_id=None, household_id=None),
|
||||
self.translator,
|
||||
)
|
||||
|
||||
@@ -38,6 +38,7 @@ class RegistrationController(BasePublicController):
|
||||
self.event_bus.dispatch(
|
||||
integration_id="registration",
|
||||
group_id=result.group_id,
|
||||
household_id=result.household_id,
|
||||
event_type=EventTypes.user_signup,
|
||||
document_data=EventUserSignupData(username=result.username, email=result.email),
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ router = APIRouter()
|
||||
@router.get("/user/name", response_model=ValidationResponse)
|
||||
def validate_user(name: str, session: Session = Depends(generate_session)):
|
||||
"""Checks if a user with the given name exists"""
|
||||
db = get_repositories(session)
|
||||
db = get_repositories(session, group_id=None, household_id=None)
|
||||
existing_element = db.users.get_one(name, "username", any_case=True)
|
||||
return ValidationResponse(valid=existing_element is None)
|
||||
|
||||
@@ -22,7 +22,7 @@ def validate_user(name: str, session: Session = Depends(generate_session)):
|
||||
@router.get("/user/email", response_model=ValidationResponse)
|
||||
def validate_user_email(email: str, session: Session = Depends(generate_session)):
|
||||
"""Checks if a user with the given name exists"""
|
||||
db = get_repositories(session)
|
||||
db = get_repositories(session, group_id=None, household_id=None)
|
||||
existing_element = db.users.get_one(email, "email", any_case=True)
|
||||
return ValidationResponse(valid=existing_element is None)
|
||||
|
||||
@@ -30,15 +30,23 @@ def validate_user_email(email: str, session: Session = Depends(generate_session)
|
||||
@router.get("/group", response_model=ValidationResponse)
|
||||
def validate_group(name: str, session: Session = Depends(generate_session)):
|
||||
"""Checks if a group with the given name exists"""
|
||||
db = get_repositories(session)
|
||||
db = get_repositories(session, group_id=None, household_id=None)
|
||||
existing_element = db.groups.get_by_name(name)
|
||||
return ValidationResponse(valid=existing_element is None)
|
||||
|
||||
|
||||
@router.get("/household", response_model=ValidationResponse)
|
||||
def validate_household(name: str, session: Session = Depends(generate_session)):
|
||||
"""Checks if a household with the given name exists"""
|
||||
db = get_repositories(session, group_id=None, household_id=None)
|
||||
existing_element = db.households.get_by_name(name)
|
||||
return ValidationResponse(valid=existing_element is None)
|
||||
|
||||
|
||||
@router.get("/recipe", response_model=ValidationResponse)
|
||||
def validate_recipe(group_id: UUID, name: str, session: Session = Depends(generate_session)):
|
||||
"""Checks if a group with the given slug exists"""
|
||||
db = get_repositories(session)
|
||||
"""Checks if a recipe with the given slug exists"""
|
||||
db = get_repositories(session, group_id=None, household_id=None)
|
||||
slug = slugify(name)
|
||||
existing_element = db.recipes.get_by_slug(group_id, slug)
|
||||
return ValidationResponse(valid=existing_element is None)
|
||||
|
||||
@@ -3,11 +3,11 @@ from .datetime_parse import DateError, DateTimeError, DurationError, TimeError
|
||||
from .mealie_model import HasUUID, MealieModel, SearchType
|
||||
|
||||
__all__ = [
|
||||
"HasUUID",
|
||||
"MealieModel",
|
||||
"SearchType",
|
||||
"DateError",
|
||||
"DateTimeError",
|
||||
"DurationError",
|
||||
"TimeError",
|
||||
"HasUUID",
|
||||
"MealieModel",
|
||||
"SearchType",
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@ from enum import Enum
|
||||
from typing import ClassVar, Protocol, TypeVar
|
||||
|
||||
from humps.main import camelize
|
||||
from pydantic import UUID4, BaseModel, ConfigDict, model_validator
|
||||
from pydantic import UUID4, AliasChoices, BaseModel, ConfigDict, Field, model_validator
|
||||
from sqlalchemy import Select, desc, func, or_, text
|
||||
from sqlalchemy.orm import InstrumentedAttribute, Session
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
@@ -20,6 +20,26 @@ T = TypeVar("T", bound=BaseModel)
|
||||
HOUR_ONLY_TZ_PATTERN = re.compile(r"[+-]\d{2}$")
|
||||
|
||||
|
||||
def UpdatedAtField(*args, **kwargs):
|
||||
"""
|
||||
Wrapper for Pydantic's Field, which sets default values for our update_at aliases
|
||||
|
||||
Since the database stores this value as update_at, we want to accept this as a possible value
|
||||
"""
|
||||
|
||||
kwargs.pop("alias", None)
|
||||
kwargs.pop("alias_generator", None)
|
||||
|
||||
# ensure the alias is not overwritten by the generator, if there is one
|
||||
# https://docs.pydantic.dev/latest/concepts/alias/#alias-priority
|
||||
kwargs["alias_priority"] = 2
|
||||
|
||||
kwargs["validation_alias"] = AliasChoices("update_at", "updateAt", "updated_at", "updatedAt")
|
||||
kwargs["serialization_alias"] = "updatedAt"
|
||||
|
||||
return Field(*args, **kwargs)
|
||||
|
||||
|
||||
class SearchType(Enum):
|
||||
fuzzy = "fuzzy"
|
||||
tokenized = "tokenized"
|
||||
@@ -77,7 +97,7 @@ class MealieModel(BaseModel):
|
||||
Cast the current model to another with additional arguments. Useful for
|
||||
transforming DTOs into models that are saved to a database
|
||||
"""
|
||||
create_data = {field: getattr(self, field) for field in self.__fields__ if field in cls.__fields__}
|
||||
create_data = {field: getattr(self, field) for field in self.model_fields if field in cls.model_fields}
|
||||
create_data.update(kwargs or {})
|
||||
return cls(**create_data)
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ __all__ = [
|
||||
"MaintenanceLogs",
|
||||
"MaintenanceStorageDetails",
|
||||
"MaintenanceSummary",
|
||||
"ChowdownURL",
|
||||
"MigrationFile",
|
||||
"MigrationImport",
|
||||
"Migrations",
|
||||
"CustomPageBase",
|
||||
"CustomPageOut",
|
||||
"CommentImport",
|
||||
"CustomPageImport",
|
||||
"GroupImport",
|
||||
@@ -28,11 +34,11 @@ __all__ = [
|
||||
"RecipeImport",
|
||||
"SettingsImport",
|
||||
"UserImport",
|
||||
"EmailReady",
|
||||
"EmailSuccess",
|
||||
"EmailTest",
|
||||
"CustomPageBase",
|
||||
"CustomPageOut",
|
||||
"AllBackups",
|
||||
"BackupFile",
|
||||
"BackupOptions",
|
||||
"CreateBackup",
|
||||
"ImportJob",
|
||||
"AdminAboutInfo",
|
||||
"AppInfo",
|
||||
"AppStartupInfo",
|
||||
@@ -40,13 +46,7 @@ __all__ = [
|
||||
"AppTheme",
|
||||
"CheckAppConfig",
|
||||
"OIDCInfo",
|
||||
"ChowdownURL",
|
||||
"MigrationFile",
|
||||
"MigrationImport",
|
||||
"Migrations",
|
||||
"AllBackups",
|
||||
"BackupFile",
|
||||
"BackupOptions",
|
||||
"CreateBackup",
|
||||
"ImportJob",
|
||||
"EmailReady",
|
||||
"EmailSuccess",
|
||||
"EmailTest",
|
||||
]
|
||||
|
||||
@@ -4,6 +4,7 @@ from mealie.schema._mealie import MealieModel
|
||||
class AppStatistics(MealieModel):
|
||||
total_recipes: int
|
||||
total_users: int
|
||||
total_households: int
|
||||
total_groups: int
|
||||
uncategorized_recipes: int
|
||||
untagged_recipes: int
|
||||
@@ -15,6 +16,7 @@ class AppInfo(MealieModel):
|
||||
demo_status: bool
|
||||
allow_signup: bool
|
||||
default_group_slug: str | None = None
|
||||
default_household_slug: str | None = None
|
||||
enable_oidc: bool
|
||||
oidc_redirect: bool
|
||||
oidc_provider_name: str
|
||||
@@ -58,6 +60,7 @@ class AdminAboutInfo(AppInfo):
|
||||
db_type: str
|
||||
db_url: str | None = None
|
||||
default_group: str
|
||||
default_household: str
|
||||
build_id: str
|
||||
recipe_scraper_version: str
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user