From b9b0a674099ce85694b96aa8a7665d94a854e42b Mon Sep 17 00:00:00 2001 From: hay-kot Date: Mon, 26 Apr 2021 16:09:43 -0800 Subject: [PATCH] Bulk assign --- docs/docs/changelog/v0.5.0.md | 5 ++++ frontend/src/api/api-utils.js | 10 +++++++ frontend/src/api/recipe.js | 6 ++++ .../src/components/UI/Dialogs/BaseDialog.vue | 2 +- .../ToolBox/CategoryTagEditor/BulkAssign.vue | 13 ++++++-- mealie/db/database.py | 20 ++++++------- mealie/db/db_base.py | 2 +- mealie/routes/recipe/category_routes.py | 18 +++++++---- mealie/routes/recipe/recipe_crud_routes.py | 30 +++++++++++++++---- mealie/routes/recipe/tag_routes.py | 18 +++++++---- 10 files changed, 91 insertions(+), 33 deletions(-) diff --git a/docs/docs/changelog/v0.5.0.md b/docs/docs/changelog/v0.5.0.md index 930b272ec..252a8568a 100644 --- a/docs/docs/changelog/v0.5.0.md +++ b/docs/docs/changelog/v0.5.0.md @@ -19,6 +19,11 @@ ## Features and Improvements ### General +- New Toolbox Page! + - Bulk assign categories and tags by keyword search + - Title case all Categories or Tags with 1 click + - Create/Rename/Delete Operations for Tags/Categories + - Remove Unused Categories or Tags with 1 click - More localization - Start date for Week is now selectable - Languages are now managed through Crowdin diff --git a/frontend/src/api/api-utils.js b/frontend/src/api/api-utils.js index 853d5ac9f..47868d0f7 100644 --- a/frontend/src/api/api-utils.js +++ b/frontend/src/api/api-utils.js @@ -39,6 +39,16 @@ const apiReq = { processResponse(response); return response; }, + patch: async function(url, data) { + let response = await axios.patch(url, data).catch(function(error) { + if (error.response) { + processResponse(error.response); + return response; + } else return; + }); + processResponse(response); + return response; + }, get: async function(url, data) { let response = await axios.get(url, data).catch(function(error) { diff --git a/frontend/src/api/recipe.js b/frontend/src/api/recipe.js index c86e5b4cf..364d6a688 100644 --- a/frontend/src/api/recipe.js +++ b/frontend/src/api/recipe.js @@ -71,6 +71,12 @@ export const recipeAPI = { return response.data; }, + async patch(data) { + let response = await apiReq.patch(recipeURLs.update(data.slug), data); + store.dispatch("requestRecentRecipes"); + return response.data; + }, + async delete(recipeSlug) { await apiReq.delete(recipeURLs.delete(recipeSlug)); store.dispatch("requestRecentRecipes"); diff --git a/frontend/src/components/UI/Dialogs/BaseDialog.vue b/frontend/src/components/UI/Dialogs/BaseDialog.vue index eed570388..83e25880e 100644 --- a/frontend/src/components/UI/Dialogs/BaseDialog.vue +++ b/frontend/src/components/UI/Dialogs/BaseDialog.vue @@ -5,7 +5,7 @@ :width="modalWidth + 'px'" :content-class="top ? 'top-dialog' : undefined" > - + {{ titleIcon }} diff --git a/frontend/src/pages/Admin/ToolBox/CategoryTagEditor/BulkAssign.vue b/frontend/src/pages/Admin/ToolBox/CategoryTagEditor/BulkAssign.vue index 209f66ee3..962adbef0 100644 --- a/frontend/src/pages/Admin/ToolBox/CategoryTagEditor/BulkAssign.vue +++ b/frontend/src/pages/Admin/ToolBox/CategoryTagEditor/BulkAssign.vue @@ -62,6 +62,7 @@ import CardSection from "@/components/UI/CardSection"; import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector"; import BaseDialog from "@/components/UI/Dialogs/BaseDialog"; +import { api } from "@/api"; export default { props: { isTags: { @@ -113,9 +114,15 @@ export default { this.tagsToAssign = []; }, assignAll() { - console.log("Categories", this.catsToAssign); - console.log("Tags", this.tagsToAssign); - console.log("results", this.results); + this.loading = true; + this.results.forEach(async element => { + element.recipeCategory = element.recipeCategory.concat( + this.catsToAssign + ); + element.tags = element.tags.concat(this.tagsToAssign); + await api.recipes.patch(element); + }); + this.loading = false; }, closeDialog() { this.$refs.assignDialog.close(); diff --git a/mealie/db/database.py b/mealie/db/database.py index 69e3830d1..b724006b7 100644 --- a/mealie/db/database.py +++ b/mealie/db/database.py @@ -1,3 +1,5 @@ +from logging import getLogger + from mealie.db.db_base import BaseDocument from mealie.db.models.group import Group from mealie.db.models.mealplan import MealPlanModel @@ -16,12 +18,13 @@ from mealie.schema.theme import SiteTheme from mealie.schema.user import GroupInDB, UserInDB from sqlalchemy.orm.session import Session +logger = getLogger() + class _Recipes(BaseDocument): def __init__(self) -> None: self.primary_key = "slug" self.sql_model: RecipeModel = RecipeModel - self.orm_mode = True self.schema: Recipe = Recipe def update_image(self, session: Session, slug: str, extension: str = None) -> str: @@ -36,23 +39,26 @@ class _Categories(BaseDocument): def __init__(self) -> None: self.primary_key = "slug" self.sql_model = Category - self.orm_mode = True self.schema = RecipeCategoryResponse + def get_empty(self, session: Session): + return session.query(Category).filter(~Category.recipes.any()).all() + class _Tags(BaseDocument): def __init__(self) -> None: self.primary_key = "slug" self.sql_model = Tag - self.orm_mode = True self.schema = RecipeTagResponse + def get_empty(self, session: Session): + return session.query(Tag).filter(~Tag.recipes.any()).all() + class _Meals(BaseDocument): def __init__(self) -> None: self.primary_key = "uid" self.sql_model = MealPlanModel - self.orm_mode = True self.schema = MealPlanInDB @@ -60,7 +66,6 @@ class _Settings(BaseDocument): def __init__(self) -> None: self.primary_key = "id" self.sql_model = SiteSettings - self.orm_mode = True self.schema = SiteSettingsSchema @@ -68,7 +73,6 @@ class _Themes(BaseDocument): def __init__(self) -> None: self.primary_key = "name" self.sql_model = SiteThemeModel - self.orm_mode = True self.schema = SiteTheme @@ -76,7 +80,6 @@ class _Users(BaseDocument): def __init__(self) -> None: self.primary_key = "id" self.sql_model = User - self.orm_mode = True self.schema = UserInDB def update_password(self, session, id, password: str): @@ -91,7 +94,6 @@ class _Groups(BaseDocument): def __init__(self) -> None: self.primary_key = "id" self.sql_model = Group - self.orm_mode = True self.schema = GroupInDB def get_meals(self, session: Session, match_value: str, match_key: str = "name") -> list[MealPlanInDB]: @@ -116,7 +118,6 @@ class _SignUps(BaseDocument): def __init__(self) -> None: self.primary_key = "token" self.sql_model = SignUp - self.orm_mode = True self.schema = SignUpOut @@ -124,7 +125,6 @@ class _CustomPages(BaseDocument): def __init__(self) -> None: self.primary_key = "id" self.sql_model = CustomPage - self.orm_mode = True self.schema = CustomPageOut diff --git a/mealie/db/db_base.py b/mealie/db/db_base.py index 8d6344fc8..ede800379 100644 --- a/mealie/db/db_base.py +++ b/mealie/db/db_base.py @@ -12,7 +12,6 @@ class BaseDocument: self.primary_key: str self.store: str self.sql_model: SqlAlchemyBase - self.orm_mode = False self.schema: BaseModel # TODO: Improve Get All Query Functionality @@ -138,3 +137,4 @@ class BaseDocument: session.delete(result) session.commit() + diff --git a/mealie/routes/recipe/category_routes.py b/mealie/routes/recipe/category_routes.py index 7774fc295..7d950459f 100644 --- a/mealie/routes/recipe/category_routes.py +++ b/mealie/routes/recipe/category_routes.py @@ -18,6 +18,18 @@ async def get_all_recipe_categories(session: Session = Depends(generate_session) return db.categories.get_all_limit_columns(session, ["slug", "name"]) +@router.get("/empty") +def get_empty_categories(session: Session = Depends(generate_session)): + """ Returns a list of categories that do not contain any recipes""" + return db.categories.get_empty(session) + + +@router.get("/{category}", response_model=RecipeCategoryResponse) +def get_all_recipes_by_category(category: str, session: Session = Depends(generate_session)): + """ Returns a list of recipes associated with the provided category. """ + return db.categories.get(session, category) + + @router.post("") async def create_recipe_category( category: CategoryIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user) @@ -27,12 +39,6 @@ async def create_recipe_category( return db.categories.create(session, category.dict()) -@router.get("/{category}", response_model=RecipeCategoryResponse) -def get_all_recipes_by_category(category: str, session: Session = Depends(generate_session)): - """ Returns a list of recipes associated with the provided category. """ - return db.categories.get(session, category) - - @router.put("/{category}", response_model=RecipeCategoryResponse) async def update_recipe_category( category: str, diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py index 6b6cfa3bf..5669fe478 100644 --- a/mealie/routes/recipe/recipe_crud_routes.py +++ b/mealie/routes/recipe/recipe_crud_routes.py @@ -1,7 +1,5 @@ -import shutil from enum import Enum -import requests from fastapi import APIRouter, Depends, File, Form, HTTPException from fastapi.responses import FileResponse from mealie.db.database import db @@ -13,10 +11,7 @@ from mealie.services.image.image import IMG_OPTIONS, delete_image, read_image, r from mealie.services.scraper.scraper import create_from_url from sqlalchemy.orm.session import Session -router = APIRouter( - prefix="/api/recipes", - tags=["Recipe CRUD"], -) +router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"]) @router.post("/create", status_code=201, response_model=str) @@ -69,6 +64,29 @@ def update_recipe( return recipe.slug +@router.patch("/{recipe_slug}") +def update_recipe( + recipe_slug: str, + data: dict, + session: Session = Depends(generate_session), + current_user=Depends(get_current_user), +): + """ Updates a recipe by existing slug and data. """ + + existing_entry: Recipe = db.recipes.get(session, recipe_slug) + + entry_dict = existing_entry.dict() + entry_dict.update(data) + updated_entry = Recipe(**entry_dict) # ! Surely there's a better way? + + recipe: Recipe = db.recipes.update(session, recipe_slug, updated_entry.dict()) + + if recipe_slug != recipe.slug: + rename_image(original_slug=recipe_slug, new_slug=recipe.slug) + + return recipe + + @router.delete("/{recipe_slug}") def delete_recipe( recipe_slug: str, diff --git a/mealie/routes/recipe/tag_routes.py b/mealie/routes/recipe/tag_routes.py index fee09c78b..509926110 100644 --- a/mealie/routes/recipe/tag_routes.py +++ b/mealie/routes/recipe/tag_routes.py @@ -20,6 +20,18 @@ async def get_all_recipe_tags(session: Session = Depends(generate_session)): return db.tags.get_all_limit_columns(session, ["slug", "name"]) +@router.get("/empty") +def get_empty_tags(session: Session = Depends(generate_session)): + """ Returns a list of tags that do not contain any recipes""" + return db.tags.get_empty(session) + + +@router.get("/{tag}", response_model=RecipeTagResponse) +def get_all_recipes_by_tag(tag: str, session: Session = Depends(generate_session)): + """ Returns a list of recipes associated with the provided tag. """ + return db.tags.get(session, tag) + + @router.post("") async def create_recipe_tag( tag: TagIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user) @@ -29,12 +41,6 @@ async def create_recipe_tag( return db.tags.create(session, tag.dict()) -@router.get("/{tag}", response_model=RecipeTagResponse) -def get_all_recipes_by_tag(tag: str, session: Session = Depends(generate_session)): - """ Returns a list of recipes associated with the provided tag. """ - return db.tags.get(session, tag) - - @router.put("/{tag}", response_model=RecipeTagResponse) async def update_recipe_tag( tag: str, new_tag: TagIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)