mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-19 13:19:41 +02:00
feat: Change Recipe Owner (#4355)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
This commit is contained in:
parent
60ea83d737
commit
1dc7b24146
11 changed files with 187 additions and 14 deletions
|
@ -34,8 +34,8 @@
|
||||||
<template #item.userId="{ item }">
|
<template #item.userId="{ item }">
|
||||||
<v-list-item class="justify-start">
|
<v-list-item class="justify-start">
|
||||||
<UserAvatar :user-id="item.userId" :tooltip="false" size="40" />
|
<UserAvatar :user-id="item.userId" :tooltip="false" size="40" />
|
||||||
<v-list-item-content>
|
<v-list-item-content class="pl-2">
|
||||||
<v-list-item-title>
|
<v-list-item-title class="text-left">
|
||||||
{{ getMember(item.userId) }}
|
{{ getMember(item.userId) }}
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="d-flex justify-start align-center">
|
<div class="d-flex justify-start align-top py-2">
|
||||||
<RecipeImageUploadBtn class="my-1" :slug="recipe.slug" @upload="uploadImage" @refresh="imageKey++" />
|
<RecipeImageUploadBtn class="my-1" :slug="recipe.slug" @upload="uploadImage" @refresh="imageKey++" />
|
||||||
<RecipeSettingsMenu
|
<RecipeSettingsMenu
|
||||||
class="my-1 mx-1"
|
class="my-1 mx-1"
|
||||||
|
@ -7,22 +7,47 @@
|
||||||
:is-owner="recipe.userId == user.id"
|
:is-owner="recipe.userId == user.id"
|
||||||
@upload="uploadImage"
|
@upload="uploadImage"
|
||||||
/>
|
/>
|
||||||
|
<v-spacer />
|
||||||
|
<v-container class="py-0" style="width: 40%;">
|
||||||
|
<v-select
|
||||||
|
v-model="recipe.userId"
|
||||||
|
:items="allUsers"
|
||||||
|
item-text="fullName"
|
||||||
|
item-value="id"
|
||||||
|
:label="$tc('general.owner')"
|
||||||
|
hide-details
|
||||||
|
:disabled="!canEditOwner"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<UserAvatar :user-id="recipe.userId" :tooltip="false" />
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
<v-card-text v-if="ownerHousehold" class="pa-0 d-flex" style="align-items: flex-end;">
|
||||||
|
<v-spacer />
|
||||||
|
<v-icon>{{ $globals.icons.household }}</v-icon>
|
||||||
|
<span class="pl-1">{{ ownerHousehold.name }}</span>
|
||||||
|
</v-card-text>
|
||||||
|
</v-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onUnmounted } from "@nuxtjs/composition-api";
|
import { computed, defineComponent, onUnmounted } from "@nuxtjs/composition-api";
|
||||||
import { clearPageState, usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
import { clearPageState, usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
||||||
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
import { Recipe } from "~/lib/api/types/recipe";
|
import { Recipe } from "~/lib/api/types/recipe";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import RecipeImageUploadBtn from "~/components/Domain/Recipe/RecipeImageUploadBtn.vue";
|
import RecipeImageUploadBtn from "~/components/Domain/Recipe/RecipeImageUploadBtn.vue";
|
||||||
import RecipeSettingsMenu from "~/components/Domain/Recipe/RecipeSettingsMenu.vue";
|
import RecipeSettingsMenu from "~/components/Domain/Recipe/RecipeSettingsMenu.vue";
|
||||||
|
import { useUserStore } from "~/composables/store/use-user-store";
|
||||||
|
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
||||||
|
import { useHouseholdStore } from "~/composables/store";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
RecipeImageUploadBtn,
|
RecipeImageUploadBtn,
|
||||||
RecipeSettingsMenu,
|
RecipeSettingsMenu,
|
||||||
|
UserAvatar,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
recipe: {
|
recipe: {
|
||||||
|
@ -34,6 +59,22 @@ export default defineComponent({
|
||||||
const { user } = usePageUser();
|
const { user } = usePageUser();
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const { imageKey } = usePageState(props.recipe.slug);
|
const { imageKey } = usePageState(props.recipe.slug);
|
||||||
|
|
||||||
|
const canEditOwner = computed(() => {
|
||||||
|
return user.id === props.recipe.userId || user.admin;
|
||||||
|
})
|
||||||
|
|
||||||
|
const { store: allUsers } = useUserStore();
|
||||||
|
const { store: households } = useHouseholdStore();
|
||||||
|
const ownerHousehold = computed(() => {
|
||||||
|
const owner = allUsers.value.find((u) => u.id === props.recipe.userId);
|
||||||
|
if (!owner) {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return households.value.find((h) => h.id === owner.householdId);
|
||||||
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearPageState(props.recipe.slug);
|
clearPageState(props.recipe.slug);
|
||||||
console.debug("reset RecipePage state during unmount");
|
console.debug("reset RecipePage state during unmount");
|
||||||
|
@ -51,8 +92,11 @@ export default defineComponent({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
|
canEditOwner,
|
||||||
uploadImage,
|
uploadImage,
|
||||||
imageKey,
|
imageKey,
|
||||||
|
allUsers,
|
||||||
|
ownerHousehold,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -182,6 +182,7 @@
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"id": "Id",
|
"id": "Id",
|
||||||
"owner": "Owner",
|
"owner": "Owner",
|
||||||
|
"change-owner": "Change Owner",
|
||||||
"date-added": "Date Added",
|
"date-added": "Date Added",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"run": "Run",
|
"run": "Run",
|
||||||
|
|
|
@ -77,6 +77,8 @@ export interface ReadWebhook {
|
||||||
}
|
}
|
||||||
export interface UserSummary {
|
export interface UserSummary {
|
||||||
id: string;
|
id: string;
|
||||||
|
groupId: string;
|
||||||
|
householdId: string;
|
||||||
username: string;
|
username: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -178,6 +178,10 @@ export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> {
|
||||||
return `${routes.recipesRecipeSlugExportZip(recipeSlug)}?token=${token}`;
|
return `${routes.recipesRecipeSlugExportZip(recipeSlug)}?token=${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateMany(payload: Recipe[]) {
|
||||||
|
return await this.requests.put<Recipe[]>(routes.recipesBase, payload);
|
||||||
|
}
|
||||||
|
|
||||||
async updateLastMade(recipeSlug: string, timestamp: string) {
|
async updateLastMade(recipeSlug: string, timestamp: string) {
|
||||||
return await this.requests.patch<Recipe, RecipeLastMade>(routes.recipesSlugLastMade(recipeSlug), { timestamp })
|
return await this.requests.patch<Recipe, RecipeLastMade>(routes.recipesSlugLastMade(recipeSlug), { timestamp })
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,24 @@
|
||||||
<i>{{ $tc('data-pages.recipes.selected-length-recipe-s-settings-will-be-updated', selected.length) }}</i>
|
<i>{{ $tc('data-pages.recipes.selected-length-recipe-s-settings-will-be-updated', selected.length) }}</i>
|
||||||
</p>
|
</p>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
<v-card-text v-else-if="dialog.mode == MODES.changeOwner">
|
||||||
|
<v-select
|
||||||
|
v-model="selectedOwner"
|
||||||
|
:items="allUsers"
|
||||||
|
item-text="fullName"
|
||||||
|
item-value="id"
|
||||||
|
:label="$tc('general.owner')"
|
||||||
|
hide-details
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<UserAvatar :user-id="selectedOwner" :tooltip="false" />
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
<v-card-text v-if="selectedOwnerHousehold" class="d-flex" style="align-items: flex-end;">
|
||||||
|
<v-icon>{{ $globals.icons.household }}</v-icon>
|
||||||
|
<span class="pl-1">{{ selectedOwnerHousehold.name }}</span>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card-text>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
<section>
|
<section>
|
||||||
<!-- Recipe Data Table -->
|
<!-- Recipe Data Table -->
|
||||||
|
@ -109,6 +127,7 @@
|
||||||
@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)"
|
@update-settings="openDialog(MODES.updateSettings)"
|
||||||
|
@change-owner="openDialog(MODES.changeOwner)"
|
||||||
>
|
>
|
||||||
</BaseOverflowButton>
|
</BaseOverflowButton>
|
||||||
|
|
||||||
|
@ -155,7 +174,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, reactive, ref, useContext, onMounted } from "@nuxtjs/composition-api";
|
import { computed, defineComponent, reactive, ref, useContext, onMounted } from "@nuxtjs/composition-api";
|
||||||
import RecipeDataTable from "~/components/Domain/Recipe/RecipeDataTable.vue";
|
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";
|
||||||
|
@ -165,6 +184,9 @@ import GroupExportData from "~/components/Domain/Group/GroupExportData.vue";
|
||||||
import { GroupDataExport } from "~/lib/api/types/group";
|
import { GroupDataExport } from "~/lib/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";
|
import RecipeSettingsSwitches from "~/components/Domain/Recipe/RecipeSettingsSwitches.vue";
|
||||||
|
import { useUserStore } from "~/composables/store/use-user-store";
|
||||||
|
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
||||||
|
import { useHouseholdStore } from "~/composables/store/use-household-store";
|
||||||
|
|
||||||
enum MODES {
|
enum MODES {
|
||||||
tag = "tag",
|
tag = "tag",
|
||||||
|
@ -172,10 +194,11 @@ enum MODES {
|
||||||
export = "export",
|
export = "export",
|
||||||
delete = "delete",
|
delete = "delete",
|
||||||
updateSettings = "updateSettings",
|
updateSettings = "updateSettings",
|
||||||
|
changeOwner = "changeOwner",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { RecipeDataTable, RecipeOrganizerSelector, GroupExportData, RecipeSettingsSwitches },
|
components: { RecipeDataTable, RecipeOrganizerSelector, GroupExportData, RecipeSettingsSwitches, UserAvatar },
|
||||||
scrollToTop: true,
|
scrollToTop: true,
|
||||||
setup() {
|
setup() {
|
||||||
const { $auth, $globals, i18n } = useContext();
|
const { $auth, $globals, i18n } = useContext();
|
||||||
|
@ -230,6 +253,11 @@ export default defineComponent({
|
||||||
text: i18n.tc("data-pages.recipes.update-settings"),
|
text: i18n.tc("data-pages.recipes.update-settings"),
|
||||||
event: "update-settings",
|
event: "update-settings",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: $globals.icons.user,
|
||||||
|
text: i18n.tc("general.change-owner"),
|
||||||
|
event: "change-owner",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: $globals.icons.delete,
|
icon: $globals.icons.delete,
|
||||||
text: i18n.tc("general.delete"),
|
text: i18n.tc("general.delete"),
|
||||||
|
@ -312,9 +340,7 @@ export default defineComponent({
|
||||||
|
|
||||||
const recipes = selected.value.map((x: Recipe) => x.slug ?? "");
|
const recipes = selected.value.map((x: Recipe) => x.slug ?? "");
|
||||||
|
|
||||||
const { response, data } = await api.bulk.bulkDelete({ recipes });
|
await api.bulk.bulkDelete({ recipes });
|
||||||
|
|
||||||
console.log(response, data);
|
|
||||||
|
|
||||||
await refreshRecipes();
|
await refreshRecipes();
|
||||||
resetAll();
|
resetAll();
|
||||||
|
@ -335,9 +361,23 @@ export default defineComponent({
|
||||||
|
|
||||||
const recipes = selected.value.map((x: Recipe) => x.slug ?? "");
|
const recipes = selected.value.map((x: Recipe) => x.slug ?? "");
|
||||||
|
|
||||||
const { response, data } = await api.bulk.bulkSetSettings({ recipes, settings: recipeSettings });
|
await api.bulk.bulkSetSettings({ recipes, settings: recipeSettings });
|
||||||
|
|
||||||
console.log(response, data);
|
await refreshRecipes();
|
||||||
|
resetAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeOwner() {
|
||||||
|
if(!selected.value.length || !selectedOwner.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selected.value.forEach((r) => {
|
||||||
|
r.userId = selectedOwner.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
await api.recipes.updateMany(selected.value);
|
||||||
|
|
||||||
await refreshRecipes();
|
await refreshRecipes();
|
||||||
resetAll();
|
resetAll();
|
||||||
|
@ -365,6 +405,7 @@ export default defineComponent({
|
||||||
[MODES.export]: i18n.tc("data-pages.recipes.export-recipes"),
|
[MODES.export]: i18n.tc("data-pages.recipes.export-recipes"),
|
||||||
[MODES.delete]: i18n.tc("data-pages.recipes.delete-recipes"),
|
[MODES.delete]: i18n.tc("data-pages.recipes.delete-recipes"),
|
||||||
[MODES.updateSettings]: i18n.tc("data-pages.recipes.update-settings"),
|
[MODES.updateSettings]: i18n.tc("data-pages.recipes.update-settings"),
|
||||||
|
[MODES.changeOwner]: i18n.tc("general.change-owner"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const callbacks: Record<MODES, () => Promise<void>> = {
|
const callbacks: Record<MODES, () => Promise<void>> = {
|
||||||
|
@ -373,6 +414,7 @@ export default defineComponent({
|
||||||
[MODES.export]: exportSelected,
|
[MODES.export]: exportSelected,
|
||||||
[MODES.delete]: deleteSelected,
|
[MODES.delete]: deleteSelected,
|
||||||
[MODES.updateSettings]: updateSettings,
|
[MODES.updateSettings]: updateSettings,
|
||||||
|
[MODES.changeOwner]: changeOwner,
|
||||||
};
|
};
|
||||||
|
|
||||||
const icons: Record<MODES, string> = {
|
const icons: Record<MODES, string> = {
|
||||||
|
@ -381,6 +423,7 @@ export default defineComponent({
|
||||||
[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,
|
[MODES.updateSettings]: $globals.icons.cog,
|
||||||
|
[MODES.changeOwner]: $globals.icons.user,
|
||||||
};
|
};
|
||||||
|
|
||||||
dialog.mode = mode;
|
dialog.mode = mode;
|
||||||
|
@ -390,6 +433,22 @@ export default defineComponent({
|
||||||
dialog.state = true;
|
dialog.state = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { store: allUsers } = useUserStore();
|
||||||
|
const { store: households } = useHouseholdStore();
|
||||||
|
const selectedOwner = ref("");
|
||||||
|
const selectedOwnerHousehold = computed(() => {
|
||||||
|
if(!selectedOwner.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const owner = allUsers.value.find((u) => u.id === selectedOwner.value);
|
||||||
|
if (!owner) {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return households.value.find((h) => h.id === owner.householdId);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
recipeSettings,
|
recipeSettings,
|
||||||
selectAll,
|
selectAll,
|
||||||
|
@ -412,6 +471,9 @@ export default defineComponent({
|
||||||
groupExports,
|
groupExports,
|
||||||
purgeExportsDialog,
|
purgeExportsDialog,
|
||||||
purgeExports,
|
purgeExports,
|
||||||
|
allUsers,
|
||||||
|
selectedOwner,
|
||||||
|
selectedOwnerHousehold,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
head() {
|
head() {
|
||||||
|
|
|
@ -79,19 +79,21 @@ class GroupCookbookController(BaseCrudController):
|
||||||
cb = self.mixins.update_one(cookbook, cookbook.id)
|
cb = self.mixins.update_one(cookbook, cookbook.id)
|
||||||
updated_by_group_and_household[cb.group_id][cb.household_id].append(cb)
|
updated_by_group_and_household[cb.group_id][cb.household_id].append(cb)
|
||||||
|
|
||||||
|
all_updated: list[ReadCookBook] = []
|
||||||
if updated_by_group_and_household:
|
if updated_by_group_and_household:
|
||||||
for group_id, household_dict in updated_by_group_and_household.items():
|
for group_id, household_dict in updated_by_group_and_household.items():
|
||||||
for household_id, updated in household_dict.items():
|
for household_id, updated_cookbooks in household_dict.items():
|
||||||
|
all_updated.extend(updated_cookbooks)
|
||||||
self.publish_event(
|
self.publish_event(
|
||||||
event_type=EventTypes.cookbook_updated,
|
event_type=EventTypes.cookbook_updated,
|
||||||
document_data=EventCookbookBulkData(
|
document_data=EventCookbookBulkData(
|
||||||
operation=EventOperation.update, cookbook_ids=[cb.id for cb in updated]
|
operation=EventOperation.update, cookbook_ids=[cb.id for cb in updated_cookbooks]
|
||||||
),
|
),
|
||||||
group_id=group_id,
|
group_id=group_id,
|
||||||
household_id=household_id,
|
household_id=household_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return updated
|
return all_updated
|
||||||
|
|
||||||
@router.get("/{item_id}", response_model=RecipeCookBook)
|
@router.get("/{item_id}", response_model=RecipeCookBook)
|
||||||
def get_one(self, item_id: UUID4 | str):
|
def get_one(self, item_id: UUID4 | str):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from collections import defaultdict
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from shutil import copyfileobj, rmtree
|
from shutil import copyfileobj, rmtree
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
@ -60,6 +61,7 @@ from mealie.schema.response.responses import ErrorResponse
|
||||||
from mealie.services import urls
|
from mealie.services import urls
|
||||||
from mealie.services.event_bus_service.event_types import (
|
from mealie.services.event_bus_service.event_types import (
|
||||||
EventOperation,
|
EventOperation,
|
||||||
|
EventRecipeBulkData,
|
||||||
EventRecipeBulkReportData,
|
EventRecipeBulkReportData,
|
||||||
EventRecipeData,
|
EventRecipeData,
|
||||||
EventTypes,
|
EventTypes,
|
||||||
|
@ -466,6 +468,31 @@ class RecipeController(BaseRecipeController):
|
||||||
|
|
||||||
return recipe
|
return recipe
|
||||||
|
|
||||||
|
@router.put("")
|
||||||
|
def update_many(self, data: list[Recipe]):
|
||||||
|
updated_by_group_and_household: defaultdict[UUID4, defaultdict[UUID4, list[Recipe]]] = defaultdict(
|
||||||
|
lambda: defaultdict(list)
|
||||||
|
)
|
||||||
|
for recipe in data:
|
||||||
|
r = self.service.update_one(recipe.id, recipe) # type: ignore
|
||||||
|
updated_by_group_and_household[r.group_id][r.household_id].append(r)
|
||||||
|
|
||||||
|
all_updated: list[Recipe] = []
|
||||||
|
if updated_by_group_and_household:
|
||||||
|
for group_id, household_dict in updated_by_group_and_household.items():
|
||||||
|
for household_id, updated_recipes in household_dict.items():
|
||||||
|
all_updated.extend(updated_recipes)
|
||||||
|
self.publish_event(
|
||||||
|
event_type=EventTypes.recipe_updated,
|
||||||
|
document_data=EventRecipeBulkData(
|
||||||
|
operation=EventOperation.update, recipe_slugs=[r.slug for r in updated_recipes]
|
||||||
|
),
|
||||||
|
group_id=group_id,
|
||||||
|
household_id=household_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return all_updated
|
||||||
|
|
||||||
@router.patch("/{slug}")
|
@router.patch("/{slug}")
|
||||||
def patch_one(self, slug: str, data: Recipe):
|
def patch_one(self, slug: str, data: Recipe):
|
||||||
"""Updates a recipe by existing slug and data."""
|
"""Updates a recipe by existing slug and data."""
|
||||||
|
|
|
@ -180,6 +180,8 @@ class UserOut(UserBase):
|
||||||
|
|
||||||
class UserSummary(MealieModel):
|
class UserSummary(MealieModel):
|
||||||
id: UUID4
|
id: UUID4
|
||||||
|
group_id: UUID4
|
||||||
|
household_id: UUID4
|
||||||
username: str
|
username: str
|
||||||
full_name: str
|
full_name: str
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
|
@ -138,6 +138,11 @@ class EventRecipeData(EventDocumentDataBase):
|
||||||
recipe_slug: str
|
recipe_slug: str
|
||||||
|
|
||||||
|
|
||||||
|
class EventRecipeBulkData(EventDocumentDataBase):
|
||||||
|
document_type: EventDocumentType = EventDocumentType.recipe
|
||||||
|
recipe_slugs: list[str]
|
||||||
|
|
||||||
|
|
||||||
class EventRecipeBulkReportData(EventDocumentDataBase):
|
class EventRecipeBulkReportData(EventDocumentDataBase):
|
||||||
document_type: EventDocumentType = EventDocumentType.recipe_bulk_report
|
document_type: EventDocumentType = EventDocumentType.recipe_bulk_report
|
||||||
report_id: UUID4
|
report_id: UUID4
|
||||||
|
|
|
@ -483,6 +483,30 @@ def test_read_update(
|
||||||
assert cats[0]["name"] in test_name
|
assert cats[0]["name"] in test_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_many(api_client: TestClient, unique_user: TestUser):
|
||||||
|
recipe_slugs = [random_string() for _ in range(3)]
|
||||||
|
for slug in recipe_slugs:
|
||||||
|
api_client.post(api_routes.recipes, json={"name": slug}, headers=unique_user.token)
|
||||||
|
|
||||||
|
recipes_data: list[dict] = [
|
||||||
|
json.loads(api_client.get(api_routes.recipes_slug(slug), headers=unique_user.token).text)
|
||||||
|
for slug in recipe_slugs
|
||||||
|
]
|
||||||
|
|
||||||
|
new_slug_by_id = {r["id"]: random_string() for r in recipes_data}
|
||||||
|
for recipe_data in recipes_data:
|
||||||
|
recipe_data["name"] = new_slug_by_id[recipe_data["id"]]
|
||||||
|
recipe_data["slug"] = new_slug_by_id[recipe_data["id"]]
|
||||||
|
|
||||||
|
response = api_client.put(api_routes.recipes, json=recipes_data, headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
for updated_recipe_data in response.json():
|
||||||
|
assert updated_recipe_data["slug"] == new_slug_by_id[updated_recipe_data["id"]]
|
||||||
|
get_response = api_client.get(api_routes.recipes_slug(updated_recipe_data["slug"]), headers=unique_user.token)
|
||||||
|
assert get_response.status_code == 200
|
||||||
|
assert get_response.json()["slug"] == updated_recipe_data["slug"]
|
||||||
|
|
||||||
|
|
||||||
def test_duplicate(api_client: TestClient, unique_user: TestUser):
|
def test_duplicate(api_client: TestClient, unique_user: TestUser):
|
||||||
recipe_data = recipe_test_data[0]
|
recipe_data = recipe_test_data[0]
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue