mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-11-04 03:28:28 -05: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 { 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";
 | 
			
		||||
 | 
			
		||||
// Many bulk actions return nothing
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
 | 
			
		||||
interface BulkActionResponse {
 | 
			
		||||
}
 | 
			
		||||
interface BulkActionResponse {}
 | 
			
		||||
 | 
			
		||||
const prefix = "/api";
 | 
			
		||||
 | 
			
		||||
@ -15,6 +14,7 @@ const routes = {
 | 
			
		||||
  bulkCategorize: prefix + "/recipes/bulk-actions/categorize",
 | 
			
		||||
  bulkTag: prefix + "/recipes/bulk-actions/tag",
 | 
			
		||||
  bulkDelete: prefix + "/recipes/bulk-actions/delete",
 | 
			
		||||
  bulkSettings: prefix + "/recipes/bulk-actions/settings",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class BulkActionsAPI extends BaseAPI {
 | 
			
		||||
@ -26,6 +26,10 @@ export class BulkActionsAPI extends BaseAPI {
 | 
			
		||||
    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) {
 | 
			
		||||
    return await this.requests.post<BulkActionResponse>(routes.bulkTag, payload);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -17,17 +17,7 @@
 | 
			
		||||
        </v-card-title>
 | 
			
		||||
        <v-divider class="mx-2"></v-divider>
 | 
			
		||||
        <v-card-text class="mt-n5 pt-6 pb-2">
 | 
			
		||||
          <v-switch
 | 
			
		||||
            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>
 | 
			
		||||
          <RecipeSettingsSwitches v-model="value" :is-owner="isOwner" />
 | 
			
		||||
        </v-card-text>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </v-menu>
 | 
			
		||||
@ -35,9 +25,11 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, useContext } from "@nuxtjs/composition-api";
 | 
			
		||||
import { defineComponent } from "@nuxtjs/composition-api";
 | 
			
		||||
import RecipeSettingsSwitches from "./RecipeSettingsSwitches.vue";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  components: { RecipeSettingsSwitches },
 | 
			
		||||
  props: {
 | 
			
		||||
    value: {
 | 
			
		||||
      type: Object,
 | 
			
		||||
@ -48,22 +40,6 @@ export default defineComponent({
 | 
			
		||||
      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>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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-card>
 | 
			
		||||
      </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>
 | 
			
		||||
    <section>
 | 
			
		||||
      <!-- Recipe Data Table -->
 | 
			
		||||
@ -100,6 +109,7 @@
 | 
			
		||||
          @tag-selected="openDialog(MODES.tag)"
 | 
			
		||||
          @categorize-selected="openDialog(MODES.category)"
 | 
			
		||||
          @delete-selected="openDialog(MODES.delete)"
 | 
			
		||||
          @update-settings="openDialog(MODES.updateSettings)"
 | 
			
		||||
        >
 | 
			
		||||
        </BaseOverflowButton>
 | 
			
		||||
 | 
			
		||||
@ -152,20 +162,22 @@ import RecipeDataTable from "~/components/Domain/Recipe/RecipeDataTable.vue";
 | 
			
		||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
 | 
			
		||||
import { useUserApi } from "~/composables/api";
 | 
			
		||||
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 { GroupDataExport } from "~/types/api-types/group";
 | 
			
		||||
import { MenuItem } from "~/components/global/BaseOverflowButton.vue";
 | 
			
		||||
import RecipeSettingsSwitches from "~/components/Domain/Recipe/RecipeSettingsSwitches.vue";
 | 
			
		||||
 | 
			
		||||
const MODES = {
 | 
			
		||||
  tag: "tag",
 | 
			
		||||
  category: "category",
 | 
			
		||||
  export: "export",
 | 
			
		||||
  delete: "delete",
 | 
			
		||||
};
 | 
			
		||||
enum MODES {
 | 
			
		||||
  tag = "tag",
 | 
			
		||||
  category = "category",
 | 
			
		||||
  export = "export",
 | 
			
		||||
  delete = "delete",
 | 
			
		||||
  updateSettings = "updateSettings",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  components: { RecipeDataTable, RecipeOrganizerSelector, GroupExportData },
 | 
			
		||||
  components: { RecipeDataTable, RecipeOrganizerSelector, GroupExportData, RecipeSettingsSwitches },
 | 
			
		||||
  scrollToTop: true,
 | 
			
		||||
  setup() {
 | 
			
		||||
    const { getAllRecipes, refreshRecipes } = useRecipes(true, true);
 | 
			
		||||
@ -217,6 +229,11 @@ export default defineComponent({
 | 
			
		||||
        text: "Categorize",
 | 
			
		||||
        event: "categorize-selected",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        icon: $globals.icons.cog,
 | 
			
		||||
        text: "Update Settings",
 | 
			
		||||
        event: "update-settings",
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        icon: $globals.icons.delete,
 | 
			
		||||
        text: "Delete",
 | 
			
		||||
@ -307,6 +324,29 @@ export default defineComponent({
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
@ -322,26 +362,29 @@ export default defineComponent({
 | 
			
		||||
      icon: $globals.icons.tags,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function openDialog(mode: string) {
 | 
			
		||||
      const titles = {
 | 
			
		||||
    function openDialog(mode: MODES) {
 | 
			
		||||
      const titles: Record<MODES, string> = {
 | 
			
		||||
        [MODES.tag]: "Tag Recipes",
 | 
			
		||||
        [MODES.category]: "Categorize Recipes",
 | 
			
		||||
        [MODES.export]: "Export Recipes",
 | 
			
		||||
        [MODES.delete]: "Delete Recipes",
 | 
			
		||||
        [MODES.updateSettings]: "Update Settings",
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const callbacks = {
 | 
			
		||||
      const callbacks: Record<MODES, () => Promise<void>> = {
 | 
			
		||||
        [MODES.tag]: tagSelected,
 | 
			
		||||
        [MODES.category]: categorizeSelected,
 | 
			
		||||
        [MODES.export]: exportSelected,
 | 
			
		||||
        [MODES.delete]: deleteSelected,
 | 
			
		||||
        [MODES.updateSettings]: updateSettings,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const icons = {
 | 
			
		||||
      const icons: Record<MODES, string> = {
 | 
			
		||||
        [MODES.tag]: $globals.icons.tags,
 | 
			
		||||
        [MODES.category]: $globals.icons.tags,
 | 
			
		||||
        [MODES.export]: $globals.icons.database,
 | 
			
		||||
        [MODES.delete]: $globals.icons.delete,
 | 
			
		||||
        [MODES.updateSettings]: $globals.icons.cog,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      dialog.mode = mode;
 | 
			
		||||
@ -352,6 +395,7 @@ export default defineComponent({
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      recipeSettings,
 | 
			
		||||
      selectAll,
 | 
			
		||||
      loading,
 | 
			
		||||
      actions,
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,19 @@ export interface CategoryBase {
 | 
			
		||||
  id: 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 {
 | 
			
		||||
  recipes: string[];
 | 
			
		||||
  tags: TagBase[];
 | 
			
		||||
@ -214,15 +227,6 @@ export interface RecipeStep {
 | 
			
		||||
  text: string;
 | 
			
		||||
  ingredientReferences?: IngredientReferences[];
 | 
			
		||||
}
 | 
			
		||||
export interface RecipeSettings {
 | 
			
		||||
  public?: boolean;
 | 
			
		||||
  showNutrition?: boolean;
 | 
			
		||||
  showAssets?: boolean;
 | 
			
		||||
  landscapeView?: boolean;
 | 
			
		||||
  disableComments?: boolean;
 | 
			
		||||
  disableAmount?: boolean;
 | 
			
		||||
  locked?: boolean;
 | 
			
		||||
}
 | 
			
		||||
export interface RecipeAsset {
 | 
			
		||||
  name: string;
 | 
			
		||||
  icon: string;
 | 
			
		||||
 | 
			
		||||
@ -5,8 +5,6 @@
 | 
			
		||||
/* 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 {
 | 
			
		||||
  message: string;
 | 
			
		||||
  error?: boolean;
 | 
			
		||||
@ -15,13 +13,6 @@ export interface ErrorResponse {
 | 
			
		||||
export interface FileTokenResponse {
 | 
			
		||||
  fileToken: string;
 | 
			
		||||
}
 | 
			
		||||
export interface PaginationQuery {
 | 
			
		||||
  page?: number;
 | 
			
		||||
  perPage?: number;
 | 
			
		||||
  orderBy?: string;
 | 
			
		||||
  orderDirection?: OrderDirection & string;
 | 
			
		||||
  queryFilter?: string;
 | 
			
		||||
}
 | 
			
		||||
export interface SuccessResponse {
 | 
			
		||||
  message: string;
 | 
			
		||||
  error?: boolean;
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import contextlib
 | 
			
		||||
import json
 | 
			
		||||
from collections.abc import Callable
 | 
			
		||||
from enum import Enum
 | 
			
		||||
@ -31,16 +32,15 @@ class MealieCrudRoute(APIRoute):
 | 
			
		||||
        original_route_handler = super().get_route_handler()
 | 
			
		||||
 | 
			
		||||
        async def custom_route_handler(request: Request) -> Response:
 | 
			
		||||
            try:
 | 
			
		||||
            with contextlib.suppress(JSONDecodeError):
 | 
			
		||||
                response = await original_route_handler(request)
 | 
			
		||||
                response_body = json.loads(response.body)
 | 
			
		||||
                if type(response_body) == dict:
 | 
			
		||||
                    if last_modified := response_body.get("updateAt"):
 | 
			
		||||
                        response.headers["last-modified"] = last_modified
 | 
			
		||||
 | 
			
		||||
            except JSONDecodeError:
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
                        # Force no-cache for all responses to prevent browser from caching API calls
 | 
			
		||||
                        response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
 | 
			
		||||
            return response
 | 
			
		||||
 | 
			
		||||
        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.routes._base import BaseUserController, controller
 | 
			
		||||
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.services.recipe.recipe_bulk_service import RecipeBulkActionsService
 | 
			
		||||
 | 
			
		||||
@ -25,6 +31,10 @@ class RecipeBulkActionsController(BaseUserController):
 | 
			
		||||
    def bulk_tag_recipes(self, tag_data: AssignTags):
 | 
			
		||||
        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")
 | 
			
		||||
    def bulk_categorize_recipes(self, assign_cats: AssignCategories):
 | 
			
		||||
        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.recipe.recipe_category import CategoryBase, TagBase
 | 
			
		||||
from mealie.schema.recipe.recipe_settings import RecipeSettings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExportTypes(str, enum.Enum):
 | 
			
		||||
@ -24,5 +25,9 @@ class AssignTags(ExportBase):
 | 
			
		||||
    tags: list[TagBase]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AssignSettings(ExportBase):
 | 
			
		||||
    settings: RecipeSettings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeleteRecipes(ExportBase):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ from mealie.repos.repository_factory import AllRepositories
 | 
			
		||||
from mealie.schema.group.group_exports import GroupDataExport
 | 
			
		||||
from mealie.schema.recipe import CategoryBase
 | 
			
		||||
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.services._base_service import BaseService
 | 
			
		||||
from mealie.services.exporter import Exporter, RecipeExporter
 | 
			
		||||
@ -47,6 +48,22 @@ class RecipeBulkActionsService(BaseService):
 | 
			
		||||
 | 
			
		||||
        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:
 | 
			
		||||
        for slug in recipes:
 | 
			
		||||
            recipe = self.repos.recipes.get_one(slug)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user