mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-12 05:15:18 -05:00
feat: ✨ add bulk actions service and routes (WIP) (#747)
* feat(frontend): ✨ Group level recipe data management * feat(backend): ✨ add bulk actions service and routes Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import all_recipe_routes, comments, image_and_assets, recipe_crud_routes, recipe_export
|
||||
from . import all_recipe_routes, bulk_actions, comments, image_and_assets, recipe_crud_routes, recipe_export
|
||||
|
||||
prefix = "/recipes"
|
||||
|
||||
@@ -11,3 +11,4 @@ router.include_router(recipe_export.user_router, prefix=prefix, tags=["Recipe: E
|
||||
router.include_router(recipe_crud_routes.user_router, prefix=prefix, tags=["Recipe: CRUD"])
|
||||
router.include_router(image_and_assets.user_router, prefix=prefix, tags=["Recipe: Images and Assets"])
|
||||
router.include_router(comments.router, prefix=prefix, tags=["Recipe: Comments"])
|
||||
router.include_router(bulk_actions.router, prefix=prefix, tags=["Recipe: Bulk Actions"])
|
||||
|
||||
49
mealie/routes/recipe/bulk_actions.py
Normal file
49
mealie/routes/recipe/bulk_actions.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from mealie.core.dependencies.dependencies import temporary_zip_path
|
||||
from mealie.schema.recipe.recipe_bulk_actions import (
|
||||
AssignCategories,
|
||||
AssignTags,
|
||||
BulkActionsResponse,
|
||||
DeleteRecipes,
|
||||
ExportRecipes,
|
||||
)
|
||||
from mealie.services.recipe.recipe_bulk_service import RecipeBulkActions
|
||||
|
||||
router = APIRouter(prefix="/bulk-actions")
|
||||
|
||||
|
||||
@router.post("/tag", response_model=BulkActionsResponse)
|
||||
def bulk_tag_recipes(
|
||||
tag_data: AssignTags,
|
||||
bulk_service: RecipeBulkActions = Depends(RecipeBulkActions.private),
|
||||
):
|
||||
bulk_service.assign_tags(tag_data.recipes, tag_data.tags)
|
||||
|
||||
|
||||
@router.post("/categorize", response_model=BulkActionsResponse)
|
||||
def bulk_categorize_recipes(
|
||||
assign_cats: AssignCategories,
|
||||
bulk_service: RecipeBulkActions = Depends(RecipeBulkActions.private),
|
||||
):
|
||||
bulk_service.assign_categories(assign_cats.recipes, assign_cats.categories)
|
||||
|
||||
|
||||
@router.post("/delete", response_model=BulkActionsResponse)
|
||||
def bulk_delete_recipes(
|
||||
delete_recipes: DeleteRecipes,
|
||||
bulk_service: RecipeBulkActions = Depends(RecipeBulkActions.private),
|
||||
):
|
||||
bulk_service.delete_recipes(delete_recipes.recipes)
|
||||
|
||||
|
||||
@router.post("/export", response_class=FileResponse)
|
||||
def bulk_export_recipes(
|
||||
export_recipes: ExportRecipes,
|
||||
temp_path=Depends(temporary_zip_path),
|
||||
bulk_service: RecipeBulkActions = Depends(RecipeBulkActions.private),
|
||||
):
|
||||
bulk_service.export_recipes(temp_path, export_recipes.recipes)
|
||||
|
||||
return FileResponse(temp_path, filename="recipes.zip")
|
||||
40
mealie/schema/recipe/recipe_bulk_actions.py
Normal file
40
mealie/schema/recipe/recipe_bulk_actions.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import enum
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
|
||||
from . import CategoryBase, TagBase
|
||||
|
||||
|
||||
class ExportTypes(str, enum.Enum):
|
||||
JSON = "json"
|
||||
|
||||
|
||||
class _ExportBase(CamelModel):
|
||||
recipes: list[str]
|
||||
|
||||
|
||||
class ExportRecipes(_ExportBase):
|
||||
export_type: ExportTypes = ExportTypes.JSON
|
||||
|
||||
|
||||
class AssignCategories(_ExportBase):
|
||||
categories: list[CategoryBase]
|
||||
|
||||
|
||||
class AssignTags(_ExportBase):
|
||||
tags: list[TagBase]
|
||||
|
||||
|
||||
class DeleteRecipes(_ExportBase):
|
||||
pass
|
||||
|
||||
|
||||
class BulkActionError(CamelModel):
|
||||
recipe: str
|
||||
error: str
|
||||
|
||||
|
||||
class BulkActionsResponse(CamelModel):
|
||||
success: bool
|
||||
message: str
|
||||
errors: list[BulkActionError] = []
|
||||
60
mealie/services/recipe/recipe_bulk_service.py
Normal file
60
mealie/services/recipe/recipe_bulk_service.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.schema.recipe import CategoryBase, Recipe
|
||||
from mealie.schema.recipe.recipe_category import TagBase
|
||||
from mealie.services._base_http_service.http_services import UserHttpService
|
||||
from mealie.services.events import create_recipe_event
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class RecipeBulkActions(UserHttpService[int, Recipe]):
|
||||
event_func = create_recipe_event
|
||||
_restrict_by_group = True
|
||||
|
||||
def populate_item(self, _: int) -> Recipe:
|
||||
return
|
||||
|
||||
def export_recipes(self, temp_path: Path, recipes: list[str]) -> None:
|
||||
return
|
||||
|
||||
def assign_tags(self, recipes: list[str], tags: list[TagBase]) -> None:
|
||||
for slug in recipes:
|
||||
recipe = self.db.recipes.get_one(slug)
|
||||
|
||||
if recipe is None:
|
||||
logger.error(f"Failed to tag recipe {slug}, no recipe found")
|
||||
|
||||
recipe.tags += tags
|
||||
|
||||
try:
|
||||
self.db.recipes.update(slug, recipe)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to tag recipe {slug}")
|
||||
logger.error(e)
|
||||
|
||||
def assign_categories(self, recipes: list[str], categories: list[CategoryBase]) -> None:
|
||||
for slug in recipes:
|
||||
recipe = self.db.recipes.get_one(slug)
|
||||
|
||||
if recipe is None:
|
||||
logger.error(f"Failed to categorize recipe {slug}, no recipe found")
|
||||
|
||||
recipe.recipe_category += categories
|
||||
|
||||
try:
|
||||
self.db.recipes.update(slug, recipe)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to categorize recipe {slug}")
|
||||
logger.error(e)
|
||||
|
||||
def delete_recipes(self, recipes: list[str]) -> None:
|
||||
for slug in recipes:
|
||||
try:
|
||||
self.db.recipes.delete(slug)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete recipe {slug}")
|
||||
logger.error(e)
|
||||
Reference in New Issue
Block a user