mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-03-04 04:33:12 -05:00
feat(backend): ✨ start multi-tenant support (WIP) (#680)
* fix ts types * feat(code-generation): ♻️ update code-generation formats * new scope * add step button * fix linter error * update code-generation tags * feat(backend): ✨ start multi-tenant support * feat(backend): ✨ group invitation token generation and signup * refactor(backend): ♻️ move group admin actions to admin router * set url base to include `/admin` * feat(frontend): ✨ generate user sign-up links * test(backend): ✅ refactor test-suite to further decouple tests (WIP) * feat(backend): 🐛 assign owner on backup import for recipes * fix(backend): 🐛 assign recipe owner on migration from other service Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
@@ -10,7 +10,6 @@ from mealie.routes.mealplans import meal_plan_router
|
||||
from mealie.routes.media import media_router
|
||||
from mealie.routes.site_settings import settings_router
|
||||
from mealie.services.events import create_general_event
|
||||
from mealie.services.recipe.all_recipe_service import subscripte_to_recipe_events
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
@@ -71,7 +70,6 @@ def system_startup():
|
||||
)
|
||||
)
|
||||
create_general_event("Application Startup", f"Mealie API started on port {settings.API_PORT}")
|
||||
subscripte_to_recipe_events()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -6,6 +6,18 @@ from mealie.schema.user.user import PrivateUser
|
||||
from .dependencies import generate_session, get_admin_user, get_current_user, is_logged_in
|
||||
|
||||
|
||||
class RequestContext:
|
||||
def __init__(
|
||||
self,
|
||||
background_tasks: BackgroundTasks,
|
||||
session: Session = Depends(generate_session),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
self.session: Session = session
|
||||
self.bg_task: BackgroundTasks = background_tasks
|
||||
self.user: bool = user
|
||||
|
||||
|
||||
class PublicDeps:
|
||||
"""
|
||||
PublicDeps contains the common dependencies for all read operations through the API.
|
||||
|
||||
@@ -52,6 +52,21 @@ class BaseAccessModel(Generic[T, D]):
|
||||
|
||||
return [eff_schema.from_orm(x) for x in session.query(self.sql_model).offset(start).limit(limit).all()]
|
||||
|
||||
def multi_query(
|
||||
self,
|
||||
session: Session,
|
||||
query_by: dict[str, str],
|
||||
start=0,
|
||||
limit: int = None,
|
||||
override_schema=None,
|
||||
) -> list[T]:
|
||||
eff_schema = override_schema or self.schema
|
||||
|
||||
return [
|
||||
eff_schema.from_orm(x)
|
||||
for x in session.query(self.sql_model).filter_by(**query_by).offset(start).limit(limit).all()
|
||||
]
|
||||
|
||||
def get_all_limit_columns(self, session: Session, fields: list[str], limit: int = None) -> list[D]:
|
||||
"""Queries the database for the selected model. Restricts return responses to the
|
||||
keys specified under "fields"
|
||||
@@ -105,11 +120,6 @@ class BaseAccessModel(Generic[T, D]):
|
||||
eff_schema = override_schema or self.schema
|
||||
return eff_schema.from_orm(result)
|
||||
|
||||
def get_many(
|
||||
self, session: Session, value: str, key: str = None, limit=1, any_case=False, override_schema=None
|
||||
) -> list[T]:
|
||||
pass
|
||||
|
||||
def get(
|
||||
self, session: Session, match_value: str, match_key: str = None, limit=1, any_case=False, override_schema=None
|
||||
) -> T | list[T]:
|
||||
|
||||
@@ -6,6 +6,7 @@ from mealie.db.data_access_layer.group_access_model import GroupDataAccessModel
|
||||
from mealie.db.models.event import Event, EventNotification
|
||||
from mealie.db.models.group import Group
|
||||
from mealie.db.models.group.cookbook import CookBook
|
||||
from mealie.db.models.group.invite_tokens import GroupInviteToken
|
||||
from mealie.db.models.group.preferences import GroupPreferencesModel
|
||||
from mealie.db.models.group.shopping_list import ShoppingList
|
||||
from mealie.db.models.group.webhooks import GroupWebhooksModel
|
||||
@@ -22,6 +23,7 @@ from mealie.schema.cookbook import ReadCookBook
|
||||
from mealie.schema.events import Event as EventSchema
|
||||
from mealie.schema.events import EventNotificationIn
|
||||
from mealie.schema.group.group_preferences import ReadGroupPreferences
|
||||
from mealie.schema.group.invite_token import ReadInviteToken
|
||||
from mealie.schema.group.webhook import ReadWebhook
|
||||
from mealie.schema.meal_plan import MealPlanOut, ShoppingListOut
|
||||
from mealie.schema.recipe import (
|
||||
@@ -87,6 +89,7 @@ class DatabaseAccessLayer:
|
||||
|
||||
# Group Data
|
||||
self.groups = GroupDataAccessModel(pk_id, Group, GroupInDB)
|
||||
self.group_tokens = BaseAccessModel("token", GroupInviteToken, ReadInviteToken)
|
||||
self.meals = BaseAccessModel(pk_id, MealPlan, MealPlanOut)
|
||||
self.webhooks = BaseAccessModel(pk_id, GroupWebhooksModel, ReadWebhook)
|
||||
self.shopping_lists = BaseAccessModel(pk_id, ShoppingList, ShoppingListOut)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from .cookbook import *
|
||||
from .group import *
|
||||
from .invite_tokens import *
|
||||
from .preferences import *
|
||||
from .shopping_list import *
|
||||
from .webhooks import *
|
||||
|
||||
@@ -3,6 +3,7 @@ import sqlalchemy.orm as orm
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.config import settings
|
||||
from mealie.db.models.group.invite_tokens import GroupInviteToken
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import auto_init
|
||||
@@ -14,11 +15,13 @@ from .preferences import GroupPreferencesModel
|
||||
|
||||
class Group(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "groups"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.String, index=True, nullable=False, unique=True)
|
||||
users = orm.relationship("User", back_populates="group")
|
||||
categories = orm.relationship(Category, secondary=group2categories, single_parent=True, uselist=True)
|
||||
|
||||
invite_tokens = orm.relationship(
|
||||
GroupInviteToken, back_populates="group", cascade="all, delete-orphan", uselist=True
|
||||
)
|
||||
preferences = orm.relationship(
|
||||
GroupPreferencesModel,
|
||||
back_populates="group",
|
||||
@@ -27,13 +30,16 @@ class Group(SqlAlchemyBase, BaseMixins):
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
# Recipes
|
||||
recipes = orm.relationship("RecipeModel", back_populates="group", uselist=True)
|
||||
|
||||
# CRUD From Others
|
||||
mealplans = orm.relationship("MealPlan", back_populates="group", single_parent=True, order_by="MealPlan.start_date")
|
||||
webhooks = orm.relationship(GroupWebhooksModel, uselist=True, cascade="all, delete-orphan")
|
||||
cookbooks = orm.relationship(CookBook, back_populates="group", single_parent=True)
|
||||
shopping_lists = orm.relationship("ShoppingList", back_populates="group", single_parent=True)
|
||||
|
||||
@auto_init({"users", "webhooks", "shopping_lists", "cookbooks", "preferences"})
|
||||
@auto_init({"users", "webhooks", "shopping_lists", "cookbooks", "preferences", "invite_tokens"})
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
||||
|
||||
|
||||
17
mealie/db/models/group/invite_tokens.py
Normal file
17
mealie/db/models/group/invite_tokens.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String, orm
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import auto_init
|
||||
|
||||
|
||||
class GroupInviteToken(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "invite_tokens"
|
||||
token = Column(String, index=True, nullable=False, unique=True)
|
||||
uses_left = Column(Integer, nullable=False, default=1)
|
||||
|
||||
group_id = Column(Integer, ForeignKey("groups.id"))
|
||||
group = orm.relationship("Group", back_populates="invite_tokens")
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_):
|
||||
pass
|
||||
@@ -8,6 +8,7 @@ from sqlalchemy.orm import validates
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import auto_init
|
||||
from ..users import users_to_favorites
|
||||
from .api_extras import ApiExtras
|
||||
from .assets import RecipeAsset
|
||||
from .category import recipes2categories
|
||||
@@ -22,6 +23,19 @@ from .tool import Tool
|
||||
|
||||
class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "recipes"
|
||||
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="recipe_slug_group_id_key"),)
|
||||
|
||||
slug = sa.Column(sa.String, index=True)
|
||||
|
||||
# ID Relationships
|
||||
group_id = sa.Column(sa.Integer, sa.ForeignKey("groups.id"))
|
||||
group = orm.relationship("Group", back_populates="recipes", foreign_keys=[group_id])
|
||||
|
||||
user_id = sa.Column(sa.Integer, sa.ForeignKey("users.id"))
|
||||
user = orm.relationship("User", uselist=False, foreign_keys=[user_id])
|
||||
|
||||
favorited_by: list = orm.relationship("User", secondary=users_to_favorites, back_populates="favorite_recipes")
|
||||
|
||||
# General Recipe Properties
|
||||
name = sa.Column(sa.String, nullable=False)
|
||||
description = sa.Column(sa.String)
|
||||
@@ -57,7 +71,6 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
comments: list = orm.relationship("RecipeComment", back_populates="recipe", cascade="all, delete, delete-orphan")
|
||||
|
||||
# Mealie Specific
|
||||
slug = sa.Column(sa.String, index=True, unique=True)
|
||||
settings = orm.relationship("RecipeSettings", uselist=False, cascade="all, delete-orphan")
|
||||
tags: list[Tag] = orm.relationship("Tag", secondary=recipes2tags, back_populates="recipes")
|
||||
notes: list[Note] = orm.relationship("Note", cascade="all, delete-orphan")
|
||||
@@ -69,10 +82,6 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
date_added = sa.Column(sa.Date, default=date.today)
|
||||
date_updated = sa.Column(sa.DateTime)
|
||||
|
||||
# Favorited By
|
||||
favorited_by_id = sa.Column(sa.Integer, sa.ForeignKey("users.id"))
|
||||
favorited_by = orm.relationship("User", back_populates="favorite_recipes")
|
||||
|
||||
class Config:
|
||||
get_attr = "slug"
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from .user_to_favorite import *
|
||||
from .users import *
|
||||
|
||||
10
mealie/db/models/users/user_to_favorite.py
Normal file
10
mealie/db/models/users/user_to_favorite.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from sqlalchemy import Column, ForeignKey, Integer, Table
|
||||
|
||||
from .._model_base import SqlAlchemyBase
|
||||
|
||||
users_to_favorites = Table(
|
||||
"users_to_favorites",
|
||||
SqlAlchemyBase.metadata,
|
||||
Column("user_id", Integer, ForeignKey("users.id")),
|
||||
Column("recipe_id", Integer, ForeignKey("recipes.id")),
|
||||
)
|
||||
@@ -1,9 +1,10 @@
|
||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
|
||||
|
||||
from mealie.core.config import settings
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models.group import Group
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from ..group import Group
|
||||
from .user_to_favorite import users_to_favorites
|
||||
|
||||
|
||||
class LongLiveToken(SqlAlchemyBase, BaseMixins):
|
||||
@@ -33,6 +34,8 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
group_id = Column(Integer, ForeignKey("groups.id"))
|
||||
group = orm.relationship("Group", back_populates="users")
|
||||
|
||||
# Recipes
|
||||
|
||||
tokens: list[LongLiveToken] = orm.relationship(
|
||||
LongLiveToken, back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
|
||||
)
|
||||
@@ -41,7 +44,10 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
"RecipeComment", back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
|
||||
)
|
||||
|
||||
favorite_recipes: list[RecipeModel] = orm.relationship(RecipeModel, back_populates="favorited_by")
|
||||
owned_recipes_id = Column(Integer, ForeignKey("recipes.id"))
|
||||
owned_recipes = orm.relationship("RecipeModel", single_parent=True, foreign_keys=[owned_recipes_id])
|
||||
|
||||
favorite_recipes = orm.relationship("RecipeModel", secondary=users_to_favorites, back_populates="favorited_by")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -65,9 +71,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
self.password = password
|
||||
self.advanced = advanced
|
||||
|
||||
self.favorite_recipes = [
|
||||
RecipeModel.get_ref(session=session, match_value=x, match_attr="slug") for x in favorite_recipes
|
||||
]
|
||||
self.favorite_recipes = []
|
||||
|
||||
if self.username is None:
|
||||
self.username = full_name
|
||||
@@ -99,10 +103,6 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
if password:
|
||||
self.password = password
|
||||
|
||||
self.favorite_recipes = [
|
||||
RecipeModel.get_ref(session=session, match_value=x, match_attr="slug") for x in favorite_recipes
|
||||
]
|
||||
|
||||
def update_password(self, password):
|
||||
self.password = password
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import admin_about, admin_log
|
||||
from . import admin_about, admin_group, admin_log
|
||||
|
||||
router = APIRouter(prefix="/admin")
|
||||
|
||||
router.include_router(admin_about.router, tags=["Admin: About"])
|
||||
router.include_router(admin_log.router, tags=["Admin: Log"])
|
||||
router.include_router(admin_group.router, tags=["Admin: Group"])
|
||||
|
||||
@@ -4,24 +4,21 @@ from sqlalchemy.orm.session import Session
|
||||
from mealie.core.dependencies import get_current_user
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
from mealie.schema.user import GroupBase, GroupInDB, PrivateUser, UpdateGroup
|
||||
from mealie.services.events import create_group_event
|
||||
|
||||
admin_router = AdminAPIRouter(prefix="/groups", tags=["Groups: CRUD"])
|
||||
user_router = UserAPIRouter(prefix="/groups", tags=["Groups: CRUD"])
|
||||
router = AdminAPIRouter(prefix="/groups")
|
||||
|
||||
|
||||
@admin_router.get("", response_model=list[GroupInDB])
|
||||
async def get_all_groups(
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
@router.get("", response_model=list[GroupInDB])
|
||||
async def get_all_groups(session: Session = Depends(generate_session)):
|
||||
""" Returns a list of all groups in the database """
|
||||
|
||||
return db.groups.get_all(session)
|
||||
|
||||
|
||||
@admin_router.post("", status_code=status.HTTP_201_CREATED, response_model=GroupInDB)
|
||||
@router.post("", status_code=status.HTTP_201_CREATED, response_model=GroupInDB)
|
||||
async def create_group(
|
||||
background_tasks: BackgroundTasks,
|
||||
group_data: GroupBase,
|
||||
@@ -37,17 +34,13 @@ async def create_group(
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@admin_router.put("/{id}")
|
||||
async def update_group_data(
|
||||
id: int,
|
||||
group_data: UpdateGroup,
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
@router.put("/{id}")
|
||||
async def update_group_data(id: int, group_data: UpdateGroup, session: Session = Depends(generate_session)):
|
||||
""" Updates a User Group """
|
||||
db.groups.update(session, id, group_data.dict())
|
||||
|
||||
|
||||
@admin_router.delete("/{id}")
|
||||
@router.delete("/{id}")
|
||||
async def delete_user_group(
|
||||
background_tasks: BackgroundTasks,
|
||||
id: int,
|
||||
@@ -6,11 +6,13 @@ from fastapi import BackgroundTasks, Depends, File, HTTPException, UploadFile, s
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.config import app_dirs
|
||||
from mealie.core.dependencies import get_current_user
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.core.security import create_file_token
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
from mealie.schema.admin import BackupJob, ImportJob, Imports, LocalBackup
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
from mealie.services.backups import imports
|
||||
from mealie.services.backups.exports import backup_all
|
||||
from mealie.services.events import create_backup_event
|
||||
@@ -82,10 +84,12 @@ def import_database(
|
||||
file_name: str,
|
||||
import_data: ImportJob,
|
||||
session: Session = Depends(generate_session),
|
||||
user: PrivateUser = Depends(get_current_user),
|
||||
):
|
||||
""" Import a database backup file generated from Mealie. """
|
||||
|
||||
db_import = imports.import_database(
|
||||
user=user,
|
||||
session=session,
|
||||
archive=import_data.name,
|
||||
import_recipes=import_data.recipes,
|
||||
|
||||
@@ -3,7 +3,7 @@ from fastapi import APIRouter
|
||||
from mealie.services._base_http_service import RouterFactory
|
||||
from mealie.services.group_services import CookbookService, WebhookService
|
||||
|
||||
from . import categories, crud, self_service
|
||||
from . import categories, invitations, preferences, self_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -13,5 +13,5 @@ router.include_router(self_service.user_router)
|
||||
router.include_router(cookbook_router)
|
||||
router.include_router(categories.user_router)
|
||||
router.include_router(webhook_router)
|
||||
router.include_router(crud.user_router)
|
||||
router.include_router(crud.admin_router)
|
||||
router.include_router(invitations.router, prefix="/groups/invitations", tags=["Groups: Invitations"])
|
||||
router.include_router(preferences.router, prefix="/groups/preferences", tags=["Group: Preferences"])
|
||||
|
||||
18
mealie/routes/groups/invitations.py
Normal file
18
mealie/routes/groups/invitations.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
|
||||
from mealie.schema.group.invite_token import CreateInviteToken, ReadInviteToken
|
||||
from mealie.services.group_services.group_service import GroupSelfService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=list[ReadInviteToken])
|
||||
def get_invite_tokens(g_service: GroupSelfService = Depends(GroupSelfService.write_existing)):
|
||||
return g_service.get_invite_tokens()
|
||||
|
||||
|
||||
@router.post("", response_model=ReadInviteToken, status_code=status.HTTP_201_CREATED)
|
||||
def create_invite_token(
|
||||
uses: CreateInviteToken, g_service: GroupSelfService = Depends(GroupSelfService.write_existing)
|
||||
):
|
||||
return g_service.create_invite_token(uses.uses)
|
||||
19
mealie/routes/groups/preferences.py
Normal file
19
mealie/routes/groups/preferences.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import Depends
|
||||
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.group.group_preferences import ReadGroupPreferences, UpdateGroupPreferences
|
||||
from mealie.services.group_services.group_service import GroupSelfService
|
||||
|
||||
router = UserAPIRouter()
|
||||
|
||||
|
||||
@router.put("", response_model=ReadGroupPreferences)
|
||||
def update_group_preferences(
|
||||
new_pref: UpdateGroupPreferences, g_service: GroupSelfService = Depends(GroupSelfService.write_existing)
|
||||
):
|
||||
return g_service.update_preferences(new_pref).preferences
|
||||
|
||||
|
||||
@router.get("", response_model=ReadGroupPreferences)
|
||||
def get_group_preferences(g_service: GroupSelfService = Depends(GroupSelfService.write_existing)):
|
||||
return g_service.item.preferences
|
||||
@@ -1,7 +1,6 @@
|
||||
from fastapi import Depends
|
||||
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.group.group_preferences import ReadGroupPreferences, UpdateGroupPreferences
|
||||
from mealie.schema.user.user import GroupInDB
|
||||
from mealie.services.group_services.group_service import GroupSelfService
|
||||
|
||||
@@ -13,15 +12,3 @@ async def get_logged_in_user_group(g_service: GroupSelfService = Depends(GroupSe
|
||||
""" Returns the Group Data for the Current User """
|
||||
|
||||
return g_service.item
|
||||
|
||||
|
||||
@user_router.put("/preferences", response_model=ReadGroupPreferences)
|
||||
def update_group_preferences(
|
||||
new_pref: UpdateGroupPreferences, g_service: GroupSelfService = Depends(GroupSelfService.write_existing)
|
||||
):
|
||||
return g_service.update_preferences(new_pref).preferences
|
||||
|
||||
|
||||
@user_router.get("/preferences", response_model=ReadGroupPreferences)
|
||||
def get_group_preferences(g_service: GroupSelfService = Depends(GroupSelfService.write_existing)):
|
||||
return g_service.item.preferences
|
||||
|
||||
@@ -25,7 +25,7 @@ async def get_recipe_img(slug: str, file_name: ImageType = ImageType.original):
|
||||
and should not hit the API in production"""
|
||||
recipe_image = Recipe(slug=slug).image_dir.joinpath(file_name.value)
|
||||
|
||||
if recipe_image:
|
||||
if recipe_image.exists():
|
||||
return FileResponse(recipe_image)
|
||||
else:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@@ -8,7 +8,9 @@ from sqlalchemy.orm.session import Session
|
||||
from mealie.core.config import app_dirs
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
from mealie.routes.users.crud import get_logged_in_user
|
||||
from mealie.schema.admin import MigrationFile, Migrations
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
from mealie.services.migrations import migration
|
||||
|
||||
router = AdminAPIRouter(prefix="/api/migrations", tags=["Migration"])
|
||||
@@ -36,10 +38,15 @@ def get_all_migration_options():
|
||||
|
||||
|
||||
@router.post("/{import_type}/{file_name}/import")
|
||||
def import_migration(import_type: migration.Migration, file_name: str, session: Session = Depends(generate_session)):
|
||||
def import_migration(
|
||||
import_type: migration.Migration,
|
||||
file_name: str,
|
||||
session: Session = Depends(generate_session),
|
||||
user: PrivateUser = Depends(get_logged_in_user),
|
||||
):
|
||||
""" Imports all the recipes in a given directory """
|
||||
file_path = app_dirs.MIGRATION_DIR.joinpath(import_type.value, file_name)
|
||||
return migration.migrate(import_type, file_path, session)
|
||||
return migration.migrate(user, import_type, file_path, session)
|
||||
|
||||
|
||||
@router.delete("/{import_type}/{file_name}/delete", status_code=status.HTTP_200_OK)
|
||||
|
||||
@@ -8,7 +8,6 @@ router = APIRouter()
|
||||
|
||||
router.include_router(all_recipe_routes.router, prefix=prefix, tags=["Recipe: Query All"])
|
||||
router.include_router(recipe_crud_routes.user_router, prefix=prefix, tags=["Recipe: CRUD"])
|
||||
router.include_router(recipe_crud_routes.public_router, prefix=prefix, tags=["Recipe: CRUD"])
|
||||
router.include_router(image_and_assets.user_router, prefix=prefix, tags=["Recipe: Images and Assets"])
|
||||
router.include_router(comments.router, prefix=prefix, tags=["Recipe: Comments"])
|
||||
router.include_router(ingredient_parser.public_router, tags=["Recipe: Ingredient Parser"])
|
||||
|
||||
@@ -4,28 +4,10 @@ from sqlalchemy.orm.session import Session
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.schema.recipe import RecipeSummary
|
||||
from mealie.services.recipe.all_recipe_service import AllRecipesService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_recipe_summary(all_recipes_service: AllRecipesService.query = Depends()):
|
||||
"""
|
||||
Returns key the recipe summary data for recipes in the database. You can perform
|
||||
slice operations to set the skip/end amounts for recipes. All recipes are sorted by the added date.
|
||||
|
||||
**Query Parameters**
|
||||
- skip: The database entry to start at. (0 Indexed)
|
||||
- end: The number of entries to return.
|
||||
|
||||
skip=2, end=10 will return entries
|
||||
|
||||
"""
|
||||
|
||||
return all_recipes_service.get_recipes()
|
||||
|
||||
|
||||
@router.get("/summary/untagged", response_model=list[RecipeSummary])
|
||||
async def get_untagged_recipes(count: bool = False, session: Session = Depends(generate_session)):
|
||||
return db.recipes.count_untagged(session, count=count, override_schema=RecipeSummary)
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import shutil
|
||||
from zipfile import ZipFile
|
||||
|
||||
from fastapi import APIRouter, Depends, File
|
||||
from fastapi import Depends, File
|
||||
from fastapi.datastructures import UploadFile
|
||||
from scrape_schema_recipe import scrape_url
|
||||
from sqlalchemy.orm.session import Session
|
||||
@@ -14,26 +14,24 @@ from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.recipe import CreateRecipeByURL, Recipe, RecipeImageTypes
|
||||
from mealie.schema.recipe.recipe import CreateRecipe
|
||||
from mealie.schema.recipe.recipe import CreateRecipe, RecipeSummary
|
||||
from mealie.services.image.image import write_image
|
||||
from mealie.services.recipe.recipe_service import RecipeService
|
||||
from mealie.services.scraper.scraper import create_from_url
|
||||
|
||||
user_router = UserAPIRouter()
|
||||
public_router = APIRouter()
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
@public_router.get("/{slug}", response_model=Recipe)
|
||||
def get_recipe(recipe_service: RecipeService = Depends(RecipeService.read_existing)):
|
||||
""" Takes in a recipe slug, returns all data for a recipe """
|
||||
return recipe_service.item
|
||||
@user_router.get("", response_model=list[RecipeSummary])
|
||||
async def get_all(start=0, limit=None, service: RecipeService = Depends(RecipeService.private)):
|
||||
return service.get_all(start, limit)
|
||||
|
||||
|
||||
@user_router.post("", status_code=201, response_model=str)
|
||||
def create_from_name(data: CreateRecipe, recipe_service: RecipeService = Depends(RecipeService.private)) -> str:
|
||||
""" Takes in a JSON string and loads data into the database as a new entry"""
|
||||
return recipe_service.create_recipe(data).slug
|
||||
return recipe_service.create_one(data).slug
|
||||
|
||||
|
||||
@user_router.post("/create-url", status_code=201, response_model=str)
|
||||
@@ -41,7 +39,7 @@ def parse_recipe_url(url: CreateRecipeByURL, recipe_service: RecipeService = Dep
|
||||
""" Takes in a URL and attempts to scrape data and load it into the database """
|
||||
|
||||
recipe = create_from_url(url.url)
|
||||
return recipe_service.create_recipe(recipe).slug
|
||||
return recipe_service.create_one(recipe).slug
|
||||
|
||||
|
||||
@user_router.post("/test-scrape-url")
|
||||
@@ -80,7 +78,13 @@ async def create_recipe_from_zip(
|
||||
return recipe
|
||||
|
||||
|
||||
@public_router.get("/{slug}/zip")
|
||||
@user_router.get("/{slug}", response_model=Recipe)
|
||||
def get_recipe(recipe_service: RecipeService = Depends(RecipeService.read_existing)):
|
||||
""" Takes in a recipe slug, returns all data for a recipe """
|
||||
return recipe_service.item
|
||||
|
||||
|
||||
@user_router.get("/{slug}/zip")
|
||||
async def get_recipe_as_zip(
|
||||
slug: str, session: Session = Depends(generate_session), temp_path=Depends(temporary_zip_path)
|
||||
):
|
||||
@@ -102,17 +106,17 @@ async def get_recipe_as_zip(
|
||||
def update_recipe(data: Recipe, recipe_service: RecipeService = Depends(RecipeService.write_existing)):
|
||||
""" Updates a recipe by existing slug and data. """
|
||||
|
||||
return recipe_service.update_recipe(data)
|
||||
return recipe_service.update_one(data)
|
||||
|
||||
|
||||
@user_router.patch("/{slug}")
|
||||
def patch_recipe(data: Recipe, recipe_service: RecipeService = Depends(RecipeService.write_existing)):
|
||||
""" Updates a recipe by existing slug and data. """
|
||||
|
||||
return recipe_service.patch_recipe(data)
|
||||
return recipe_service.patch_one(data)
|
||||
|
||||
|
||||
@user_router.delete("/{slug}")
|
||||
def delete_recipe(recipe_service: RecipeService = Depends(RecipeService.write_existing)):
|
||||
""" Deletes a recipe by slug """
|
||||
return recipe_service.delete_recipe()
|
||||
return recipe_service.delete_one()
|
||||
|
||||
@@ -18,27 +18,27 @@ async def get_all(
|
||||
|
||||
|
||||
@router.post("", response_model=IngredientFood, status_code=status.HTTP_201_CREATED)
|
||||
async def create_unit(unit: CreateIngredientFood, session: Session = Depends(generate_session)):
|
||||
async def create_food(unit: CreateIngredientFood, session: Session = Depends(generate_session)):
|
||||
""" Create unit in the Database """
|
||||
|
||||
return db.ingredient_foods.create(session, unit)
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_unit(id: str, session: Session = Depends(generate_session)):
|
||||
async def get_food(id: str, session: Session = Depends(generate_session)):
|
||||
""" Get unit from the Database """
|
||||
|
||||
return db.ingredient_foods.get(session, id)
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_unit(id: str, unit: CreateIngredientFood, session: Session = Depends(generate_session)):
|
||||
async def update_food(id: str, unit: CreateIngredientFood, session: Session = Depends(generate_session)):
|
||||
""" Update unit in the Database """
|
||||
|
||||
return db.ingredient_foods.update(session, id, unit)
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_unit(id: str, session: Session = Depends(generate_session)):
|
||||
async def delete_food(id: str, session: Session = Depends(generate_session)):
|
||||
""" Delete unit from the Database """
|
||||
return db.ingredient_foods.delete(session, id)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import api_tokens, crud, favorites, images, passwords, registration, sign_up
|
||||
from . import api_tokens, crud, favorites, images, passwords, registration
|
||||
|
||||
# Must be used because of the way FastAPI works with nested routes
|
||||
user_prefix = "/users"
|
||||
@@ -9,9 +9,6 @@ router = APIRouter()
|
||||
|
||||
router.include_router(registration.router, prefix=user_prefix, tags=["Users: Registration"])
|
||||
|
||||
router.include_router(sign_up.admin_router, prefix=user_prefix, tags=["Users: Sign-Up"])
|
||||
router.include_router(sign_up.public_router, prefix=user_prefix, tags=["Users: Sign-Up"])
|
||||
|
||||
router.include_router(crud.user_router, prefix=user_prefix, tags=["Users: CRUD"])
|
||||
router.include_router(crud.admin_router, prefix=user_prefix, tags=["Users: CRUD"])
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ router = APIRouter(prefix="/register")
|
||||
|
||||
|
||||
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
|
||||
def reset_user_password(
|
||||
def register_new_user(
|
||||
data: CreateUserRegistration, registration_service: RegistrationService = Depends(RegistrationService.public)
|
||||
):
|
||||
return registration_service.register_user(data)
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.dependencies import get_admin_user
|
||||
from mealie.core.security import hash_password
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
from mealie.schema.user import PrivateUser, SignUpIn, SignUpOut, SignUpToken, UserIn
|
||||
from mealie.services.events import create_user_event
|
||||
|
||||
public_router = APIRouter(prefix="/sign-ups")
|
||||
admin_router = AdminAPIRouter(prefix="/sign-ups")
|
||||
|
||||
|
||||
@admin_router.get("", response_model=list[SignUpOut])
|
||||
async def get_all_open_sign_ups(session: Session = Depends(generate_session)):
|
||||
""" Returns a list of open sign up links """
|
||||
|
||||
return db.sign_ups.get_all(session)
|
||||
|
||||
|
||||
@admin_router.post("", response_model=SignUpToken)
|
||||
async def create_user_sign_up_key(
|
||||
background_tasks: BackgroundTasks,
|
||||
key_data: SignUpIn,
|
||||
current_user: PrivateUser = Depends(get_admin_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Generates a Random Token that a new user can sign up with """
|
||||
|
||||
sign_up = {
|
||||
"token": str(uuid.uuid1().hex),
|
||||
"name": key_data.name,
|
||||
"admin": key_data.admin,
|
||||
}
|
||||
|
||||
background_tasks.add_task(
|
||||
create_user_event, "Sign-up Token Created", f"Created by {current_user.full_name}", session=session
|
||||
)
|
||||
return db.sign_ups.create(session, sign_up)
|
||||
|
||||
|
||||
@public_router.post("/{token}")
|
||||
async def create_user_with_token(
|
||||
background_tasks: BackgroundTasks, token: str, new_user: UserIn, session: Session = Depends(generate_session)
|
||||
):
|
||||
""" Creates a user with a valid sign up token """
|
||||
|
||||
# Validate Token
|
||||
db_entry: SignUpOut = db.sign_ups.get(session, token, limit=1)
|
||||
if not db_entry:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Create User
|
||||
new_user.admin = db_entry.admin
|
||||
new_user.password = hash_password(new_user.password)
|
||||
db.users.create(session, new_user.dict())
|
||||
|
||||
# DeleteToken
|
||||
background_tasks.add_task(
|
||||
create_user_event, "Sign-up Token Used", f"New User {new_user.full_name}", session=session
|
||||
)
|
||||
db.sign_ups.delete(session, token)
|
||||
|
||||
|
||||
@admin_router.delete("/{token}")
|
||||
async def delete_token(token: str, session: Session = Depends(generate_session)):
|
||||
""" Removed a token from the database """
|
||||
db.sign_ups.delete(session, token)
|
||||
20
mealie/schema/group/invite_token.py
Normal file
20
mealie/schema/group/invite_token.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from fastapi_camelcase import CamelModel
|
||||
|
||||
|
||||
class CreateInviteToken(CamelModel):
|
||||
uses: int
|
||||
|
||||
|
||||
class SaveInviteToken(CamelModel):
|
||||
uses_left: int
|
||||
group_id: int
|
||||
token: str
|
||||
|
||||
|
||||
class ReadInviteToken(CamelModel):
|
||||
token: str
|
||||
uses_left: int
|
||||
group_id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
@@ -32,11 +32,15 @@ class CreateRecipe(CamelModel):
|
||||
|
||||
class RecipeSummary(CamelModel):
|
||||
id: Optional[int]
|
||||
|
||||
user_id: int = 0
|
||||
group_id: int = 0
|
||||
|
||||
name: Optional[str]
|
||||
slug: str = ""
|
||||
image: Optional[Any]
|
||||
|
||||
description: Optional[str]
|
||||
description: Optional[str] = ""
|
||||
recipe_category: Optional[list[str]] = []
|
||||
tags: Optional[list[str]] = []
|
||||
rating: Optional[int]
|
||||
@@ -112,32 +116,6 @@ class Recipe(RecipeSummary):
|
||||
"extras": {x.key_name: x.value for x in name_orm.extras},
|
||||
}
|
||||
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "Chicken and Rice With Leeks and Salsa Verde",
|
||||
"description": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
|
||||
"image": "chicken-and-rice-with-leeks-and-salsa-verde.jpg",
|
||||
"recipe_yield": "4 Servings",
|
||||
"recipe_ingredient": [
|
||||
"1 1/2 lb. skinless, boneless chicken thighs (4-8 depending on size)",
|
||||
"Kosher salt, freshly ground pepper",
|
||||
"3 Tbsp. unsalted butter, divided",
|
||||
],
|
||||
"recipe_instructions": [
|
||||
{
|
||||
"text": "Season chicken with salt and pepper.",
|
||||
},
|
||||
],
|
||||
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
|
||||
"tags": ["favorite", "yummy!"],
|
||||
"recipe_category": ["Dinner", "Pasta"],
|
||||
"notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
|
||||
"org_url": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
|
||||
"rating": 3,
|
||||
"extras": {"message": "Don't forget to defrost the chicken!"},
|
||||
}
|
||||
}
|
||||
|
||||
@validator("slug", always=True, pre=True)
|
||||
def validate_slug(slug: str, values):
|
||||
if not values.get("name"):
|
||||
|
||||
@@ -81,6 +81,7 @@ class UserIn(UserBase):
|
||||
class UserOut(UserBase):
|
||||
id: int
|
||||
group: str
|
||||
group_id: int
|
||||
tokens: Optional[list[LongLiveTokenOut]]
|
||||
favorite_recipes: Optional[list[str]] = []
|
||||
|
||||
@@ -112,6 +113,7 @@ class UserFavorites(UserBase):
|
||||
|
||||
class PrivateUser(UserOut):
|
||||
password: str
|
||||
group_id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@@ -27,6 +27,7 @@ from mealie.services.image import minify
|
||||
class ImportDatabase:
|
||||
def __init__(
|
||||
self,
|
||||
user: PrivateUser,
|
||||
session: Session,
|
||||
zip_archive: str,
|
||||
force_import: bool = False,
|
||||
@@ -41,6 +42,7 @@ class ImportDatabase:
|
||||
Raises:
|
||||
Exception: If the zip file does not exists an exception raise.
|
||||
"""
|
||||
self.user = user
|
||||
self.session = session
|
||||
self.archive = app_dirs.BACKUP_DIR.joinpath(zip_archive)
|
||||
self.force_imports = force_import
|
||||
@@ -66,6 +68,9 @@ class ImportDatabase:
|
||||
for recipe in recipes:
|
||||
recipe: Recipe
|
||||
|
||||
recipe.group_id = self.user.group_id
|
||||
recipe.user_id = self.user.id
|
||||
|
||||
import_status = self.import_model(
|
||||
db_table=db.recipes,
|
||||
model=recipe,
|
||||
@@ -308,6 +313,7 @@ class ImportDatabase:
|
||||
|
||||
def import_database(
|
||||
session: Session,
|
||||
user: PrivateUser,
|
||||
archive,
|
||||
import_recipes=True,
|
||||
import_settings=True,
|
||||
@@ -317,7 +323,7 @@ def import_database(
|
||||
force_import: bool = False,
|
||||
rebase: bool = False,
|
||||
):
|
||||
import_session = ImportDatabase(session, archive, force_import)
|
||||
import_session = ImportDatabase(user, session, archive, force_import)
|
||||
|
||||
recipe_report = []
|
||||
if import_recipes:
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
|
||||
from mealie.core.dependencies.grouped import UserDeps
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.schema.group.group_preferences import UpdateGroupPreferences
|
||||
from mealie.schema.group.invite_token import ReadInviteToken, SaveInviteToken
|
||||
from mealie.schema.recipe.recipe_category import CategoryBase
|
||||
from mealie.schema.user.user import GroupInDB
|
||||
from mealie.services._base_http_service.http_services import UserHttpService
|
||||
@@ -50,3 +53,10 @@ class GroupSelfService(UserHttpService[int, str]):
|
||||
self.db.group_preferences.update(self.session, self.group_id, new_preferences)
|
||||
|
||||
return self.populate_item()
|
||||
|
||||
def create_invite_token(self, uses: int = 1) -> None:
|
||||
token = SaveInviteToken(uses_left=uses, group_id=self.group_id, token=uuid4().hex)
|
||||
return self.db.group_tokens.create(self.session, token)
|
||||
|
||||
def get_invite_tokens(self) -> list[ReadInviteToken]:
|
||||
return self.db.group_tokens.multi_query(self.session, {"group_id": self.group_id})
|
||||
|
||||
@@ -10,6 +10,7 @@ from mealie.core import root_logger
|
||||
from mealie.db.database import db
|
||||
from mealie.schema.admin import MigrationImport
|
||||
from mealie.schema.recipe import Recipe
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
from mealie.services.image import image
|
||||
from mealie.services.scraper import cleaner
|
||||
from mealie.utils.unzip import unpack_zip
|
||||
@@ -34,6 +35,8 @@ class MigrationBase(BaseModel):
|
||||
session: Optional[Any]
|
||||
key_aliases: Optional[list[MigrationAlias]]
|
||||
|
||||
user: PrivateUser
|
||||
|
||||
@property
|
||||
def temp_dir(self) -> TemporaryDirectory:
|
||||
"""unpacks the migration_file into a temporary directory
|
||||
@@ -162,6 +165,10 @@ class MigrationBase(BaseModel):
|
||||
"""
|
||||
|
||||
for recipe in validated_recipes:
|
||||
|
||||
recipe.user_id = self.user.id
|
||||
recipe.group_id = self.user.group_id
|
||||
|
||||
exception = ""
|
||||
status = False
|
||||
try:
|
||||
|
||||
@@ -5,6 +5,7 @@ from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.config import app_dirs
|
||||
from mealie.schema.admin import MigrationImport
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
from mealie.services.migrations import helpers
|
||||
from mealie.services.migrations._migration_base import MigrationAlias, MigrationBase
|
||||
|
||||
@@ -18,8 +19,8 @@ class ChowdownMigration(MigrationBase):
|
||||
]
|
||||
|
||||
|
||||
def migrate(session: Session, zip_path: Path) -> list[MigrationImport]:
|
||||
cd_migration = ChowdownMigration(migration_file=zip_path, session=session)
|
||||
def migrate(user: PrivateUser, session: Session, zip_path: Path) -> list[MigrationImport]:
|
||||
cd_migration = ChowdownMigration(user=user, migration_file=zip_path, session=session)
|
||||
|
||||
with cd_migration.temp_dir as dir:
|
||||
chow_dir = next(Path(dir).iterdir())
|
||||
|
||||
@@ -19,7 +19,7 @@ class Migration(str, Enum):
|
||||
chowdown = "chowdown"
|
||||
|
||||
|
||||
def migrate(migration_type: str, file_path: Path, session: Session) -> list[MigrationImport]:
|
||||
def migrate(user, migration_type: str, file_path: Path, session: Session) -> list[MigrationImport]:
|
||||
"""The new entry point for accessing migrations within the 'migrations' service.
|
||||
Using the 'Migrations' enum class as a selector for migration_type to direct which function
|
||||
to call. All migrations will return a MigrationImport object that is built for displaying
|
||||
@@ -37,10 +37,10 @@ def migrate(migration_type: str, file_path: Path, session: Session) -> list[Migr
|
||||
logger.info(f"Starting Migration from {migration_type}")
|
||||
|
||||
if migration_type == Migration.nextcloud.value:
|
||||
migration_imports = nextcloud.migrate(session, file_path)
|
||||
migration_imports = nextcloud.migrate(user, session, file_path)
|
||||
|
||||
elif migration_type == Migration.chowdown.value:
|
||||
migration_imports = chowdown.migrate(session, file_path)
|
||||
migration_imports = chowdown.migrate(user, session, file_path)
|
||||
|
||||
else:
|
||||
return []
|
||||
|
||||
@@ -6,6 +6,7 @@ from slugify import slugify
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.schema.admin import MigrationImport
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
from mealie.services.migrations import helpers
|
||||
from mealie.services.migrations._migration_base import MigrationAlias, MigrationBase
|
||||
|
||||
@@ -42,9 +43,9 @@ class NextcloudMigration(MigrationBase):
|
||||
]
|
||||
|
||||
|
||||
def migrate(session: Session, zip_path: Path) -> list[MigrationImport]:
|
||||
def migrate(user: PrivateUser, session: Session, zip_path: Path) -> list[MigrationImport]:
|
||||
|
||||
nc_migration = NextcloudMigration(migration_file=zip_path, session=session)
|
||||
nc_migration = NextcloudMigration(user=user, migration_file=zip_path, session=session)
|
||||
|
||||
with nc_migration.temp_dir as dir:
|
||||
potential_recipe_dirs = NextcloudMigration.glob_walker(dir, glob_str="**/[!.]*.json", return_parent=True)
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import json
|
||||
from functools import lru_cache
|
||||
|
||||
from fastapi import Depends, Response
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.dependencies import is_logged_in
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import SessionLocal, generate_session
|
||||
from mealie.schema.recipe import RecipeSummary
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
class AllRecipesService:
|
||||
def __init__(self, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)):
|
||||
self.start = 0
|
||||
self.limit = 9999
|
||||
self.session = session or SessionLocal()
|
||||
self.is_user = is_user
|
||||
|
||||
@classmethod
|
||||
def query(
|
||||
cls, start=0, limit=9999, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
|
||||
):
|
||||
set_query = cls(session, is_user)
|
||||
|
||||
set_query.start = start
|
||||
set_query.limit = limit
|
||||
|
||||
return set_query
|
||||
|
||||
def get_recipes(self):
|
||||
if self.is_user:
|
||||
return get_all_recipes_user(self.limit, self.start)
|
||||
|
||||
else:
|
||||
return get_all_recipes_public(self.limit, self.start)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_all_recipes_user(limit, start):
|
||||
with SessionLocal() as session:
|
||||
all_recipes: list[RecipeSummary] = db.recipes.get_all(
|
||||
session, limit=limit, start=start, order_by="date_updated", override_schema=RecipeSummary
|
||||
)
|
||||
all_recipes_json = [recipe.dict() for recipe in all_recipes]
|
||||
return Response(content=json.dumps(jsonable_encoder(all_recipes_json)), media_type="application/json")
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_all_recipes_public(limit, start):
|
||||
with SessionLocal() as session:
|
||||
all_recipes: list[RecipeSummary] = db.recipes.get_all_public(
|
||||
session, limit=limit, start=start, order_by="date_updated", override_schema=RecipeSummary
|
||||
)
|
||||
all_recipes_json = [recipe.dict() for recipe in all_recipes]
|
||||
return Response(content=json.dumps(jsonable_encoder(all_recipes_json)), media_type="application/json")
|
||||
|
||||
|
||||
def clear_all_cache():
|
||||
get_all_recipes_user.cache_clear()
|
||||
get_all_recipes_public.cache_clear()
|
||||
logger.info("All Recipes Cache Cleared")
|
||||
|
||||
|
||||
def subscripte_to_recipe_events():
|
||||
db.recipes.subscribe(clear_all_cache)
|
||||
logger.info("All Recipes Subscribed to Database Events")
|
||||
16
mealie/services/recipe/mixins.py
Normal file
16
mealie/services/recipe/mixins.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from mealie.schema.recipe import Recipe
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
|
||||
|
||||
def recipe_creation_factory(user: PrivateUser, name: str, additional_attrs: dict = None) -> Recipe:
|
||||
"""
|
||||
The main creation point for recipes. The factor method returns an instance of the
|
||||
Recipe Schema class with the appropriate defaults set. Recipes shoudld not be created
|
||||
else-where to avoid conflicts.
|
||||
"""
|
||||
additional_attrs = additional_attrs or {}
|
||||
additional_attrs["name"] = name
|
||||
additional_attrs["user_id"] = user.id
|
||||
additional_attrs["group_id"] = user.group_id
|
||||
|
||||
return Recipe(**additional_attrs)
|
||||
@@ -7,14 +7,15 @@ from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from mealie.core.dependencies.grouped import PublicDeps, UserDeps
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.schema.recipe.recipe import CreateRecipe, Recipe
|
||||
from mealie.services._base_http_service.http_services import PublicHttpService
|
||||
from mealie.schema.recipe.recipe import CreateRecipe, Recipe, RecipeSummary
|
||||
from mealie.services._base_http_service.http_services import UserHttpService
|
||||
from mealie.services.events import create_recipe_event
|
||||
from mealie.services.recipe.mixins import recipe_creation_factory
|
||||
|
||||
logger = get_logger(module=__name__)
|
||||
|
||||
|
||||
class RecipeService(PublicHttpService[str, Recipe]):
|
||||
class RecipeService(UserHttpService[str, Recipe]):
|
||||
"""
|
||||
Class Methods:
|
||||
`read_existing`: Reads an existing recipe from the database.
|
||||
@@ -46,9 +47,13 @@ class RecipeService(PublicHttpService[str, Recipe]):
|
||||
return self.item
|
||||
|
||||
# CRUD METHODS
|
||||
def create_recipe(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe:
|
||||
if isinstance(create_data, CreateRecipe):
|
||||
create_data = Recipe(name=create_data.name)
|
||||
def get_all(self, start=0, limit=None):
|
||||
return self.db.recipes.multi_query(
|
||||
self.session, {"group_id": self.user.group_id}, start=start, limit=limit, override_schema=RecipeSummary
|
||||
)
|
||||
|
||||
def create_one(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe:
|
||||
create_data = recipe_creation_factory(self.user, name=create_data.name, additional_attrs=create_data.dict())
|
||||
|
||||
try:
|
||||
self.item = self.db.recipes.create(self.session, create_data)
|
||||
@@ -56,13 +61,13 @@ class RecipeService(PublicHttpService[str, Recipe]):
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "RECIPE_ALREADY_EXISTS"})
|
||||
|
||||
self._create_event(
|
||||
"Recipe Created (URL)",
|
||||
"Recipe Created",
|
||||
f"'{self.item.name}' by {self.user.username} \n {self.settings.BASE_URL}/recipe/{self.item.slug}",
|
||||
)
|
||||
|
||||
return self.item
|
||||
|
||||
def update_recipe(self, update_data: Recipe) -> Recipe:
|
||||
def update_one(self, update_data: Recipe) -> Recipe:
|
||||
original_slug = self.item.slug
|
||||
|
||||
try:
|
||||
@@ -74,7 +79,7 @@ class RecipeService(PublicHttpService[str, Recipe]):
|
||||
|
||||
return self.item
|
||||
|
||||
def patch_recipe(self, patch_data: Recipe) -> Recipe:
|
||||
def patch_one(self, patch_data: Recipe) -> Recipe:
|
||||
original_slug = self.item.slug
|
||||
|
||||
try:
|
||||
@@ -88,16 +93,7 @@ class RecipeService(PublicHttpService[str, Recipe]):
|
||||
|
||||
return self.item
|
||||
|
||||
def delete_recipe(self) -> Recipe:
|
||||
"""removes a recipe from the database and purges the existing files from the filesystem.
|
||||
|
||||
Raises:
|
||||
HTTPException: 400 Bad Request
|
||||
|
||||
Returns:
|
||||
Recipe: The deleted recipe
|
||||
"""
|
||||
|
||||
def delete_one(self) -> Recipe:
|
||||
try:
|
||||
recipe: Recipe = self.db.recipes.delete(self.session, self.item.slug)
|
||||
self._delete_assets()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.core.security import hash_password
|
||||
from mealie.schema.group.group_preferences import CreateGroupPreferences
|
||||
@@ -20,13 +22,37 @@ class RegistrationService(PublicHttpService[int, str]):
|
||||
self.registration = registration
|
||||
|
||||
logger.info(f"Registering user {registration.username}")
|
||||
token_entry = None
|
||||
|
||||
if registration.group:
|
||||
group = self._create_new_group()
|
||||
else:
|
||||
group = self._existing_group_ref()
|
||||
group = self._register_new_group()
|
||||
|
||||
return self._create_new_user(group)
|
||||
elif registration.group_token and registration.group_token != "":
|
||||
|
||||
token_entry = self.db.group_tokens.get(self.session, registration.group_token)
|
||||
|
||||
print("Token Entry", token_entry)
|
||||
|
||||
if not token_entry:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Invalid group token"})
|
||||
|
||||
group = self.db.groups.get(self.session, token_entry.group_id)
|
||||
|
||||
else:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Missing group"})
|
||||
|
||||
user = self._create_new_user(group)
|
||||
|
||||
if token_entry and user:
|
||||
token_entry.uses_left = token_entry.uses_left - 1
|
||||
|
||||
if token_entry.uses_left == 0:
|
||||
self.db.group_tokens.delete(self.session, token_entry.token)
|
||||
|
||||
else:
|
||||
self.db.group_tokens.update(self.session, token_entry.token, token_entry)
|
||||
|
||||
return user
|
||||
|
||||
def _create_new_user(self, group: GroupInDB) -> PrivateUser:
|
||||
new_user = UserIn(
|
||||
@@ -40,7 +66,7 @@ class RegistrationService(PublicHttpService[int, str]):
|
||||
|
||||
return self.db.users.create(self.session, new_user)
|
||||
|
||||
def _create_new_group(self) -> GroupInDB:
|
||||
def _register_new_group(self) -> GroupInDB:
|
||||
group_data = GroupBase(name=self.registration.group)
|
||||
|
||||
group_preferences = CreateGroupPreferences(
|
||||
@@ -56,6 +82,3 @@ class RegistrationService(PublicHttpService[int, str]):
|
||||
)
|
||||
|
||||
return create_new_group(self.session, group_data, group_preferences)
|
||||
|
||||
def _existing_group_ref(self) -> GroupInDB:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user