mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:28:00 -04:00 
			
		
		
		
	feat: bulk recipe settings update (#1557)
* extract switches from menu component * implement bulk updater for settings * fix browser cache api calls issue * add frontend for bulk settings modifications
This commit is contained in:
		
							parent
							
								
									5cfff75dbe
								
							
						
					
					
						commit
						7adcc86d03
					
				| @ -1,11 +1,10 @@ | |||||||
| import { BaseAPI } from "../_base"; | import { BaseAPI } from "../_base"; | ||||||
| import { AssignCategories, AssignTags, DeleteRecipes, ExportRecipes } from "~/types/api-types/recipe"; | import { AssignCategories, AssignSettings, AssignTags, DeleteRecipes, ExportRecipes } from "~/types/api-types/recipe"; | ||||||
| import { GroupDataExport } from "~/types/api-types/group"; | import { GroupDataExport } from "~/types/api-types/group"; | ||||||
| 
 | 
 | ||||||
| // Many bulk actions return nothing
 | // Many bulk actions return nothing
 | ||||||
| // eslint-disable-next-line @typescript-eslint/no-empty-interface
 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
 | ||||||
| interface BulkActionResponse { | interface BulkActionResponse {} | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| const prefix = "/api"; | const prefix = "/api"; | ||||||
| 
 | 
 | ||||||
| @ -15,6 +14,7 @@ const routes = { | |||||||
|   bulkCategorize: prefix + "/recipes/bulk-actions/categorize", |   bulkCategorize: prefix + "/recipes/bulk-actions/categorize", | ||||||
|   bulkTag: prefix + "/recipes/bulk-actions/tag", |   bulkTag: prefix + "/recipes/bulk-actions/tag", | ||||||
|   bulkDelete: prefix + "/recipes/bulk-actions/delete", |   bulkDelete: prefix + "/recipes/bulk-actions/delete", | ||||||
|  |   bulkSettings: prefix + "/recipes/bulk-actions/settings", | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export class BulkActionsAPI extends BaseAPI { | export class BulkActionsAPI extends BaseAPI { | ||||||
| @ -26,6 +26,10 @@ export class BulkActionsAPI extends BaseAPI { | |||||||
|     return await this.requests.post<BulkActionResponse>(routes.bulkCategorize, payload); |     return await this.requests.post<BulkActionResponse>(routes.bulkCategorize, payload); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async bulkSetSettings(payload: AssignSettings) { | ||||||
|  |     return await this.requests.post<BulkActionResponse>(routes.bulkSettings, payload); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async bulkTag(payload: AssignTags) { |   async bulkTag(payload: AssignTags) { | ||||||
|     return await this.requests.post<BulkActionResponse>(routes.bulkTag, payload); |     return await this.requests.post<BulkActionResponse>(routes.bulkTag, payload); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -17,17 +17,7 @@ | |||||||
|         </v-card-title> |         </v-card-title> | ||||||
|         <v-divider class="mx-2"></v-divider> |         <v-divider class="mx-2"></v-divider> | ||||||
|         <v-card-text class="mt-n5 pt-6 pb-2"> |         <v-card-text class="mt-n5 pt-6 pb-2"> | ||||||
|           <v-switch |           <RecipeSettingsSwitches v-model="value" :is-owner="isOwner" /> | ||||||
|             v-for="(itemValue, key) in value" |  | ||||||
|             :key="key" |  | ||||||
|             v-model="value[key]" |  | ||||||
|             xs |  | ||||||
|             dense |  | ||||||
|             :disabled="key == 'locked' && !isOwner" |  | ||||||
|             class="my-1" |  | ||||||
|             :label="labels[key]" |  | ||||||
|             hide-details |  | ||||||
|           ></v-switch> |  | ||||||
|         </v-card-text> |         </v-card-text> | ||||||
|       </v-card> |       </v-card> | ||||||
|     </v-menu> |     </v-menu> | ||||||
| @ -35,9 +25,11 @@ | |||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent, useContext } from "@nuxtjs/composition-api"; | import { defineComponent } from "@nuxtjs/composition-api"; | ||||||
|  | import RecipeSettingsSwitches from "./RecipeSettingsSwitches.vue"; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|  |   components: { RecipeSettingsSwitches }, | ||||||
|   props: { |   props: { | ||||||
|     value: { |     value: { | ||||||
|       type: Object, |       type: Object, | ||||||
| @ -48,22 +40,6 @@ export default defineComponent({ | |||||||
|       required: false, |       required: false, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   setup() { |  | ||||||
|     const { i18n } = useContext(); |  | ||||||
|     const labels = { |  | ||||||
|       public: i18n.t("recipe.public-recipe"), |  | ||||||
|       showNutrition: i18n.t("recipe.show-nutrition-values"), |  | ||||||
|       showAssets: i18n.t("asset.show-assets"), |  | ||||||
|       landscapeView: i18n.t("recipe.landscape-view-coming-soon"), |  | ||||||
|       disableComments: i18n.t("recipe.disable-comments"), |  | ||||||
|       disableAmount: i18n.t("recipe.disable-amount"), |  | ||||||
|       locked: i18n.t("recipe.locked"), |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|       labels, |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										51
									
								
								frontend/components/Domain/Recipe/RecipeSettingsSwitches.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								frontend/components/Domain/Recipe/RecipeSettingsSwitches.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <v-switch | ||||||
|  |       v-for="(_, key) in value" | ||||||
|  |       :key="key" | ||||||
|  |       v-model="value[key]" | ||||||
|  |       xs | ||||||
|  |       dense | ||||||
|  |       :disabled="key == 'locked' && !isOwner" | ||||||
|  |       class="my-1" | ||||||
|  |       :label="labels[key]" | ||||||
|  |       hide-details | ||||||
|  |     ></v-switch> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineComponent, useContext } from "@nuxtjs/composition-api"; | ||||||
|  | import { RecipeSettings } from "~/types/api-types/recipe"; | ||||||
|  | 
 | ||||||
|  | export default defineComponent({ | ||||||
|  |   props: { | ||||||
|  |     value: { | ||||||
|  |       type: Object as () => RecipeSettings, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     isOwner: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: false, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { i18n } = useContext(); | ||||||
|  |     const labels: Record<keyof RecipeSettings, string> = { | ||||||
|  |       public: i18n.tc("recipe.public-recipe"), | ||||||
|  |       showNutrition: i18n.tc("recipe.show-nutrition-values"), | ||||||
|  |       showAssets: i18n.tc("asset.show-assets"), | ||||||
|  |       landscapeView: i18n.tc("recipe.landscape-view-coming-soon"), | ||||||
|  |       disableComments: i18n.tc("recipe.disable-comments"), | ||||||
|  |       disableAmount: i18n.tc("recipe.disable-amount"), | ||||||
|  |       locked: i18n.tc("recipe.locked"), | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       labels, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped></style> | ||||||
| @ -55,6 +55,15 @@ | |||||||
|           </v-virtual-scroll> |           </v-virtual-scroll> | ||||||
|         </v-card> |         </v-card> | ||||||
|       </v-card-text> |       </v-card-text> | ||||||
|  |       <v-card-text v-else-if="dialog.mode == MODES.updateSettings" class="px-12"> | ||||||
|  |         <p>Settings chosen here, excluding the locked option, will be applied to all selected recipes.</p> | ||||||
|  |         <div class="mx-auto"> | ||||||
|  |           <RecipeSettingsSwitches v-model="recipeSettings" /> | ||||||
|  |         </div> | ||||||
|  |         <p class="text-center mb-0"> | ||||||
|  |           <i>{{ selected.length }} recipe(s) settings will be updated.</i> | ||||||
|  |         </p> | ||||||
|  |       </v-card-text> | ||||||
|     </BaseDialog> |     </BaseDialog> | ||||||
|     <section> |     <section> | ||||||
|       <!-- Recipe Data Table --> |       <!-- Recipe Data Table --> | ||||||
| @ -100,6 +109,7 @@ | |||||||
|           @tag-selected="openDialog(MODES.tag)" |           @tag-selected="openDialog(MODES.tag)" | ||||||
|           @categorize-selected="openDialog(MODES.category)" |           @categorize-selected="openDialog(MODES.category)" | ||||||
|           @delete-selected="openDialog(MODES.delete)" |           @delete-selected="openDialog(MODES.delete)" | ||||||
|  |           @update-settings="openDialog(MODES.updateSettings)" | ||||||
|         > |         > | ||||||
|         </BaseOverflowButton> |         </BaseOverflowButton> | ||||||
| 
 | 
 | ||||||
| @ -152,20 +162,22 @@ import RecipeDataTable from "~/components/Domain/Recipe/RecipeDataTable.vue"; | |||||||
| import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue"; | import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue"; | ||||||
| import { useUserApi } from "~/composables/api"; | import { useUserApi } from "~/composables/api"; | ||||||
| import { useRecipes, allRecipes } from "~/composables/recipes"; | import { useRecipes, allRecipes } from "~/composables/recipes"; | ||||||
| import { Recipe } from "~/types/api-types/recipe"; | import { Recipe, RecipeSettings } from "~/types/api-types/recipe"; | ||||||
| import GroupExportData from "~/components/Domain/Group/GroupExportData.vue"; | import GroupExportData from "~/components/Domain/Group/GroupExportData.vue"; | ||||||
| import { GroupDataExport } from "~/types/api-types/group"; | import { GroupDataExport } from "~/types/api-types/group"; | ||||||
| import { MenuItem } from "~/components/global/BaseOverflowButton.vue"; | import { MenuItem } from "~/components/global/BaseOverflowButton.vue"; | ||||||
|  | import RecipeSettingsSwitches from "~/components/Domain/Recipe/RecipeSettingsSwitches.vue"; | ||||||
| 
 | 
 | ||||||
| const MODES = { | enum MODES { | ||||||
|   tag: "tag", |   tag = "tag", | ||||||
|   category: "category", |   category = "category", | ||||||
|   export: "export", |   export = "export", | ||||||
|   delete: "delete", |   delete = "delete", | ||||||
| }; |   updateSettings = "updateSettings", | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|   components: { RecipeDataTable, RecipeOrganizerSelector, GroupExportData }, |   components: { RecipeDataTable, RecipeOrganizerSelector, GroupExportData, RecipeSettingsSwitches }, | ||||||
|   scrollToTop: true, |   scrollToTop: true, | ||||||
|   setup() { |   setup() { | ||||||
|     const { getAllRecipes, refreshRecipes } = useRecipes(true, true); |     const { getAllRecipes, refreshRecipes } = useRecipes(true, true); | ||||||
| @ -217,6 +229,11 @@ export default defineComponent({ | |||||||
|         text: "Categorize", |         text: "Categorize", | ||||||
|         event: "categorize-selected", |         event: "categorize-selected", | ||||||
|       }, |       }, | ||||||
|  |       { | ||||||
|  |         icon: $globals.icons.cog, | ||||||
|  |         text: "Update Settings", | ||||||
|  |         event: "update-settings", | ||||||
|  |       }, | ||||||
|       { |       { | ||||||
|         icon: $globals.icons.delete, |         icon: $globals.icons.delete, | ||||||
|         text: "Delete", |         text: "Delete", | ||||||
| @ -307,6 +324,29 @@ export default defineComponent({ | |||||||
|       resetAll(); |       resetAll(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     const recipeSettings = reactive<RecipeSettings>({ | ||||||
|  |       public: false, | ||||||
|  |       showNutrition: false, | ||||||
|  |       showAssets: false, | ||||||
|  |       landscapeView: false, | ||||||
|  |       disableComments: false, | ||||||
|  |       disableAmount: false, | ||||||
|  |       locked: false, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     async function updateSettings() { | ||||||
|  |       loading.value = true; | ||||||
|  | 
 | ||||||
|  |       const recipes = selected.value.map((x: Recipe) => x.slug ?? ""); | ||||||
|  | 
 | ||||||
|  |       const { response, data } = await api.bulk.bulkSetSettings({ recipes, settings: recipeSettings }); | ||||||
|  | 
 | ||||||
|  |       console.log(response, data); | ||||||
|  | 
 | ||||||
|  |       await refreshRecipes(); | ||||||
|  |       resetAll(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     // ============================================================ |     // ============================================================ | ||||||
|     // Dialog Management |     // Dialog Management | ||||||
| 
 | 
 | ||||||
| @ -322,26 +362,29 @@ export default defineComponent({ | |||||||
|       icon: $globals.icons.tags, |       icon: $globals.icons.tags, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     function openDialog(mode: string) { |     function openDialog(mode: MODES) { | ||||||
|       const titles = { |       const titles: Record<MODES, string> = { | ||||||
|         [MODES.tag]: "Tag Recipes", |         [MODES.tag]: "Tag Recipes", | ||||||
|         [MODES.category]: "Categorize Recipes", |         [MODES.category]: "Categorize Recipes", | ||||||
|         [MODES.export]: "Export Recipes", |         [MODES.export]: "Export Recipes", | ||||||
|         [MODES.delete]: "Delete Recipes", |         [MODES.delete]: "Delete Recipes", | ||||||
|  |         [MODES.updateSettings]: "Update Settings", | ||||||
|       }; |       }; | ||||||
| 
 | 
 | ||||||
|       const callbacks = { |       const callbacks: Record<MODES, () => Promise<void>> = { | ||||||
|         [MODES.tag]: tagSelected, |         [MODES.tag]: tagSelected, | ||||||
|         [MODES.category]: categorizeSelected, |         [MODES.category]: categorizeSelected, | ||||||
|         [MODES.export]: exportSelected, |         [MODES.export]: exportSelected, | ||||||
|         [MODES.delete]: deleteSelected, |         [MODES.delete]: deleteSelected, | ||||||
|  |         [MODES.updateSettings]: updateSettings, | ||||||
|       }; |       }; | ||||||
| 
 | 
 | ||||||
|       const icons = { |       const icons: Record<MODES, string> = { | ||||||
|         [MODES.tag]: $globals.icons.tags, |         [MODES.tag]: $globals.icons.tags, | ||||||
|         [MODES.category]: $globals.icons.tags, |         [MODES.category]: $globals.icons.tags, | ||||||
|         [MODES.export]: $globals.icons.database, |         [MODES.export]: $globals.icons.database, | ||||||
|         [MODES.delete]: $globals.icons.delete, |         [MODES.delete]: $globals.icons.delete, | ||||||
|  |         [MODES.updateSettings]: $globals.icons.cog, | ||||||
|       }; |       }; | ||||||
| 
 | 
 | ||||||
|       dialog.mode = mode; |       dialog.mode = mode; | ||||||
| @ -352,6 +395,7 @@ export default defineComponent({ | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|  |       recipeSettings, | ||||||
|       selectAll, |       selectAll, | ||||||
|       loading, |       loading, | ||||||
|       actions, |       actions, | ||||||
|  | |||||||
| @ -18,6 +18,19 @@ export interface CategoryBase { | |||||||
|   id: string; |   id: string; | ||||||
|   slug: string; |   slug: string; | ||||||
| } | } | ||||||
|  | export interface AssignSettings { | ||||||
|  |   recipes: string[]; | ||||||
|  |   settings: RecipeSettings; | ||||||
|  | } | ||||||
|  | export interface RecipeSettings { | ||||||
|  |   public?: boolean; | ||||||
|  |   showNutrition?: boolean; | ||||||
|  |   showAssets?: boolean; | ||||||
|  |   landscapeView?: boolean; | ||||||
|  |   disableComments?: boolean; | ||||||
|  |   disableAmount?: boolean; | ||||||
|  |   locked?: boolean; | ||||||
|  | } | ||||||
| export interface AssignTags { | export interface AssignTags { | ||||||
|   recipes: string[]; |   recipes: string[]; | ||||||
|   tags: TagBase[]; |   tags: TagBase[]; | ||||||
| @ -214,15 +227,6 @@ export interface RecipeStep { | |||||||
|   text: string; |   text: string; | ||||||
|   ingredientReferences?: IngredientReferences[]; |   ingredientReferences?: IngredientReferences[]; | ||||||
| } | } | ||||||
| export interface RecipeSettings { |  | ||||||
|   public?: boolean; |  | ||||||
|   showNutrition?: boolean; |  | ||||||
|   showAssets?: boolean; |  | ||||||
|   landscapeView?: boolean; |  | ||||||
|   disableComments?: boolean; |  | ||||||
|   disableAmount?: boolean; |  | ||||||
|   locked?: boolean; |  | ||||||
| } |  | ||||||
| export interface RecipeAsset { | export interface RecipeAsset { | ||||||
|   name: string; |   name: string; | ||||||
|   icon: string; |   icon: string; | ||||||
|  | |||||||
| @ -5,8 +5,6 @@ | |||||||
| /* Do not modify it by hand - just update the pydantic models and then re-run the script | /* Do not modify it by hand - just update the pydantic models and then re-run the script | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
| export type OrderDirection = "asc" | "desc"; |  | ||||||
| 
 |  | ||||||
| export interface ErrorResponse { | export interface ErrorResponse { | ||||||
|   message: string; |   message: string; | ||||||
|   error?: boolean; |   error?: boolean; | ||||||
| @ -15,13 +13,6 @@ export interface ErrorResponse { | |||||||
| export interface FileTokenResponse { | export interface FileTokenResponse { | ||||||
|   fileToken: string; |   fileToken: string; | ||||||
| } | } | ||||||
| export interface PaginationQuery { |  | ||||||
|   page?: number; |  | ||||||
|   perPage?: number; |  | ||||||
|   orderBy?: string; |  | ||||||
|   orderDirection?: OrderDirection & string; |  | ||||||
|   queryFilter?: string; |  | ||||||
| } |  | ||||||
| export interface SuccessResponse { | export interface SuccessResponse { | ||||||
|   message: string; |   message: string; | ||||||
|   error?: boolean; |   error?: boolean; | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | import contextlib | ||||||
| import json | import json | ||||||
| from collections.abc import Callable | from collections.abc import Callable | ||||||
| from enum import Enum | from enum import Enum | ||||||
| @ -31,16 +32,15 @@ class MealieCrudRoute(APIRoute): | |||||||
|         original_route_handler = super().get_route_handler() |         original_route_handler = super().get_route_handler() | ||||||
| 
 | 
 | ||||||
|         async def custom_route_handler(request: Request) -> Response: |         async def custom_route_handler(request: Request) -> Response: | ||||||
|             try: |             with contextlib.suppress(JSONDecodeError): | ||||||
|                 response = await original_route_handler(request) |                 response = await original_route_handler(request) | ||||||
|                 response_body = json.loads(response.body) |                 response_body = json.loads(response.body) | ||||||
|                 if type(response_body) == dict: |                 if type(response_body) == dict: | ||||||
|                     if last_modified := response_body.get("updateAt"): |                     if last_modified := response_body.get("updateAt"): | ||||||
|                         response.headers["last-modified"] = last_modified |                         response.headers["last-modified"] = last_modified | ||||||
| 
 | 
 | ||||||
|             except JSONDecodeError: |                         # Force no-cache for all responses to prevent browser from caching API calls | ||||||
|                 pass |                         response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" | ||||||
| 
 |  | ||||||
|             return response |             return response | ||||||
| 
 | 
 | ||||||
|         return custom_route_handler |         return custom_route_handler | ||||||
|  | |||||||
| @ -7,7 +7,13 @@ from mealie.core.dependencies.dependencies import temporary_zip_path | |||||||
| from mealie.core.security import create_file_token | from mealie.core.security import create_file_token | ||||||
| from mealie.routes._base import BaseUserController, controller | from mealie.routes._base import BaseUserController, controller | ||||||
| from mealie.schema.group.group_exports import GroupDataExport | from mealie.schema.group.group_exports import GroupDataExport | ||||||
| from mealie.schema.recipe.recipe_bulk_actions import AssignCategories, AssignTags, DeleteRecipes, ExportRecipes | from mealie.schema.recipe.recipe_bulk_actions import ( | ||||||
|  |     AssignCategories, | ||||||
|  |     AssignSettings, | ||||||
|  |     AssignTags, | ||||||
|  |     DeleteRecipes, | ||||||
|  |     ExportRecipes, | ||||||
|  | ) | ||||||
| from mealie.schema.response.responses import SuccessResponse | from mealie.schema.response.responses import SuccessResponse | ||||||
| from mealie.services.recipe.recipe_bulk_service import RecipeBulkActionsService | from mealie.services.recipe.recipe_bulk_service import RecipeBulkActionsService | ||||||
| 
 | 
 | ||||||
| @ -25,6 +31,10 @@ class RecipeBulkActionsController(BaseUserController): | |||||||
|     def bulk_tag_recipes(self, tag_data: AssignTags): |     def bulk_tag_recipes(self, tag_data: AssignTags): | ||||||
|         self.service.assign_tags(tag_data.recipes, tag_data.tags) |         self.service.assign_tags(tag_data.recipes, tag_data.tags) | ||||||
| 
 | 
 | ||||||
|  |     @router.post("/settings") | ||||||
|  |     def bulk_settings_recipes(self, settings_data: AssignSettings): | ||||||
|  |         self.service.set_settings(settings_data.recipes, settings_data.settings) | ||||||
|  | 
 | ||||||
|     @router.post("/categorize") |     @router.post("/categorize") | ||||||
|     def bulk_categorize_recipes(self, assign_cats: AssignCategories): |     def bulk_categorize_recipes(self, assign_cats: AssignCategories): | ||||||
|         self.service.assign_categories(assign_cats.recipes, assign_cats.categories) |         self.service.assign_categories(assign_cats.recipes, assign_cats.categories) | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ import enum | |||||||
| 
 | 
 | ||||||
| from mealie.schema._mealie import MealieModel | from mealie.schema._mealie import MealieModel | ||||||
| from mealie.schema.recipe.recipe_category import CategoryBase, TagBase | from mealie.schema.recipe.recipe_category import CategoryBase, TagBase | ||||||
|  | from mealie.schema.recipe.recipe_settings import RecipeSettings | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ExportTypes(str, enum.Enum): | class ExportTypes(str, enum.Enum): | ||||||
| @ -24,5 +25,9 @@ class AssignTags(ExportBase): | |||||||
|     tags: list[TagBase] |     tags: list[TagBase] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class AssignSettings(ExportBase): | ||||||
|  |     settings: RecipeSettings | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class DeleteRecipes(ExportBase): | class DeleteRecipes(ExportBase): | ||||||
|     pass |     pass | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ from mealie.repos.repository_factory import AllRepositories | |||||||
| from mealie.schema.group.group_exports import GroupDataExport | from mealie.schema.group.group_exports import GroupDataExport | ||||||
| from mealie.schema.recipe import CategoryBase | from mealie.schema.recipe import CategoryBase | ||||||
| from mealie.schema.recipe.recipe_category import TagBase | from mealie.schema.recipe.recipe_category import TagBase | ||||||
|  | from mealie.schema.recipe.recipe_settings import RecipeSettings | ||||||
| from mealie.schema.user.user import GroupInDB, PrivateUser | from mealie.schema.user.user import GroupInDB, PrivateUser | ||||||
| from mealie.services._base_service import BaseService | from mealie.services._base_service import BaseService | ||||||
| from mealie.services.exporter import Exporter, RecipeExporter | from mealie.services.exporter import Exporter, RecipeExporter | ||||||
| @ -47,6 +48,22 @@ class RecipeBulkActionsService(BaseService): | |||||||
| 
 | 
 | ||||||
|         return exports_deleted |         return exports_deleted | ||||||
| 
 | 
 | ||||||
|  |     def set_settings(self, recipes: list[str], settings: RecipeSettings) -> None: | ||||||
|  |         for slug in recipes: | ||||||
|  |             recipe = self.repos.recipes.get_one(slug) | ||||||
|  | 
 | ||||||
|  |             if recipe is None: | ||||||
|  |                 self.logger.error(f"Failed to set settings for recipe {slug}, no recipe found") | ||||||
|  | 
 | ||||||
|  |             settings.locked = recipe.settings.locked | ||||||
|  |             recipe.settings = settings | ||||||
|  | 
 | ||||||
|  |             try: | ||||||
|  |                 self.repos.recipes.update(slug, recipe) | ||||||
|  |             except Exception as e: | ||||||
|  |                 self.logger.error(f"Failed to set settings for recipe {slug}") | ||||||
|  |                 self.logger.error(e) | ||||||
|  | 
 | ||||||
|     def assign_tags(self, recipes: list[str], tags: list[TagBase]) -> None: |     def assign_tags(self, recipes: list[str], tags: list[TagBase]) -> None: | ||||||
|         for slug in recipes: |         for slug in recipes: | ||||||
|             recipe = self.repos.recipes.get_one(slug) |             recipe = self.repos.recipes.get_one(slug) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user