mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-31 20:25:14 -04:00
Fix: Allow Last Made to be Updated on Locked Recipes (#2140)
* allow certain props to be updated on locked recipe * pytest * added "last_made" to hardcoded datetime fields * refactored last made to its own route * codegen/types * updated pytest
This commit is contained in:
parent
a6c46a7420
commit
fd03d468d4
@ -126,8 +126,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
// we also update the recipe's last made value
|
// we also update the recipe's last made value
|
||||||
if (!props.value || newTimelineEvent.value.timestamp > props.value) {
|
if (!props.value || newTimelineEvent.value.timestamp > props.value) {
|
||||||
const payload = {lastMade: newTimelineEvent.value.timestamp};
|
actions.push(userApi.recipes.updateLastMade(props.recipeSlug, newTimelineEvent.value.timestamp));
|
||||||
actions.push(userApi.recipes.patchOne(props.recipeSlug, payload));
|
|
||||||
|
|
||||||
// update recipe in parent so the user can see it
|
// update recipe in parent so the user can see it
|
||||||
// we remove the trailing "Z" since this is how the API returns it
|
// we remove the trailing "Z" since this is how the API returns it
|
||||||
|
@ -303,6 +303,9 @@ export interface RecipeCommentUpdate {
|
|||||||
export interface RecipeDuplicate {
|
export interface RecipeDuplicate {
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
export interface RecipeLastMade {
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
export interface RecipeShareToken {
|
export interface RecipeShareToken {
|
||||||
recipeId: string;
|
recipeId: string;
|
||||||
expiresAt?: string;
|
expiresAt?: string;
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
ParsedIngredient,
|
ParsedIngredient,
|
||||||
UpdateImageResponse,
|
UpdateImageResponse,
|
||||||
RecipeZipTokenResponse,
|
RecipeZipTokenResponse,
|
||||||
|
RecipeLastMade,
|
||||||
RecipeTimelineEventIn,
|
RecipeTimelineEventIn,
|
||||||
RecipeTimelineEventOut,
|
RecipeTimelineEventOut,
|
||||||
RecipeTimelineEventUpdate,
|
RecipeTimelineEventUpdate,
|
||||||
@ -48,6 +49,8 @@ const routes = {
|
|||||||
recipesSlugComments: (slug: string) => `${prefix}/recipes/${slug}/comments`,
|
recipesSlugComments: (slug: string) => `${prefix}/recipes/${slug}/comments`,
|
||||||
recipesSlugCommentsId: (slug: string, id: number) => `${prefix}/recipes/${slug}/comments/${id}`,
|
recipesSlugCommentsId: (slug: string, id: number) => `${prefix}/recipes/${slug}/comments/${id}`,
|
||||||
|
|
||||||
|
recipesSlugLastMade: (slug: string) => `${prefix}/recipes/${slug}/last-made`,
|
||||||
|
|
||||||
recipesSlugTimelineEvent: (slug: string) => `${prefix}/recipes/${slug}/timeline/events`,
|
recipesSlugTimelineEvent: (slug: string) => `${prefix}/recipes/${slug}/timeline/events`,
|
||||||
recipesSlugTimelineEventId: (slug: string, id: string) => `${prefix}/recipes/${slug}/timeline/events/${id}`,
|
recipesSlugTimelineEventId: (slug: string, id: string) => `${prefix}/recipes/${slug}/timeline/events/${id}`,
|
||||||
};
|
};
|
||||||
@ -164,6 +167,10 @@ export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> {
|
|||||||
return await this.requests.post(routes.recipesCreateFromOcr, formData);
|
return await this.requests.post(routes.recipesCreateFromOcr, formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateLastMade(recipeSlug: string, timestamp: string) {
|
||||||
|
return await this.requests.patch<Recipe, RecipeLastMade>(routes.recipesSlugLastMade(recipeSlug), { timestamp })
|
||||||
|
}
|
||||||
|
|
||||||
async createTimelineEvent(recipeSlug: string, payload: RecipeTimelineEventIn) {
|
async createTimelineEvent(recipeSlug: string, payload: RecipeTimelineEventIn) {
|
||||||
return await this.requests.post<RecipeTimelineEventOut>(routes.recipesSlugTimelineEvent(recipeSlug), payload);
|
return await this.requests.post<RecipeTimelineEventOut>(routes.recipesSlugTimelineEvent(recipeSlug), payload);
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,12 @@ from mealie.routes._base.mixins import HttpRepo
|
|||||||
from mealie.routes._base.routers import MealieCrudRoute, UserAPIRouter
|
from mealie.routes._base.routers import MealieCrudRoute, UserAPIRouter
|
||||||
from mealie.schema.cookbook.cookbook import ReadCookBook
|
from mealie.schema.cookbook.cookbook import ReadCookBook
|
||||||
from mealie.schema.recipe import Recipe, RecipeImageTypes, ScrapeRecipe
|
from mealie.schema.recipe import Recipe, RecipeImageTypes, ScrapeRecipe
|
||||||
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary
|
from mealie.schema.recipe.recipe import (
|
||||||
|
CreateRecipe,
|
||||||
|
CreateRecipeByUrlBulk,
|
||||||
|
RecipeLastMade,
|
||||||
|
RecipeSummary,
|
||||||
|
)
|
||||||
from mealie.schema.recipe.recipe_asset import RecipeAsset
|
from mealie.schema.recipe.recipe_asset import RecipeAsset
|
||||||
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient
|
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient
|
||||||
from mealie.schema.recipe.recipe_scraper import ScrapeRecipeTest
|
from mealie.schema.recipe.recipe_scraper import ScrapeRecipeTest
|
||||||
@ -367,6 +372,28 @@ class RecipeController(BaseRecipeController):
|
|||||||
|
|
||||||
return recipe
|
return recipe
|
||||||
|
|
||||||
|
@router.patch("/{slug}/last-made")
|
||||||
|
def update_last_made(self, slug: str, data: RecipeLastMade):
|
||||||
|
"""Update a recipe's last made timestamp"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
recipe = self.service.update_last_made(slug, data.timestamp)
|
||||||
|
except Exception as e:
|
||||||
|
self.handle_exceptions(e)
|
||||||
|
|
||||||
|
if recipe:
|
||||||
|
self.publish_event(
|
||||||
|
event_type=EventTypes.recipe_updated,
|
||||||
|
document_data=EventRecipeData(operation=EventOperation.update, recipe_slug=recipe.slug),
|
||||||
|
message=self.t(
|
||||||
|
"notifications.generic-updated-with-url",
|
||||||
|
name=recipe.name,
|
||||||
|
url=urls.recipe_url(recipe.slug, self.settings.BASE_URL),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return recipe
|
||||||
|
|
||||||
@router.delete("/{slug}")
|
@router.delete("/{slug}")
|
||||||
def delete_one(self, slug: str):
|
def delete_one(self, slug: str):
|
||||||
"""Deletes a recipe by slug"""
|
"""Deletes a recipe by slug"""
|
||||||
|
@ -200,6 +200,10 @@ class Recipe(RecipeSummary):
|
|||||||
return user_id
|
return user_id
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeLastMade(BaseModel):
|
||||||
|
timestamp: datetime.datetime
|
||||||
|
|
||||||
|
|
||||||
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient # noqa: E402
|
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient # noqa: E402
|
||||||
|
|
||||||
RecipeSummary.update_forward_refs()
|
RecipeSummary.update_forward_refs()
|
||||||
|
@ -16,7 +16,7 @@ class AlchemyExporter(BaseService):
|
|||||||
engine: base.Engine
|
engine: base.Engine
|
||||||
meta: MetaData
|
meta: MetaData
|
||||||
|
|
||||||
look_for_datetime = {"created_at", "update_at", "date_updated", "timestamp", "expires_at", "locked_at"}
|
look_for_datetime = {"created_at", "update_at", "date_updated", "timestamp", "expires_at", "locked_at", "last_made"}
|
||||||
look_for_date = {"date_added", "date"}
|
look_for_date = {"date_added", "date"}
|
||||||
look_for_time = {"scheduled_time"}
|
look_for_time = {"scheduled_time"}
|
||||||
|
|
||||||
|
@ -247,6 +247,7 @@ class RecipeService(BaseService):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
slug (str): recipe slug
|
slug (str): recipe slug
|
||||||
|
new_data (Recipe): the new recipe data
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
exceptions.PermissionDenied (403)
|
exceptions.PermissionDenied (403)
|
||||||
@ -275,7 +276,7 @@ class RecipeService(BaseService):
|
|||||||
|
|
||||||
def patch_one(self, slug: str, patch_data: Recipe) -> Recipe:
|
def patch_one(self, slug: str, patch_data: Recipe) -> Recipe:
|
||||||
recipe: Recipe | None = self._pre_update_check(slug, patch_data)
|
recipe: Recipe | None = self._pre_update_check(slug, patch_data)
|
||||||
recipe = self.repos.recipes.by_group(self.group.id).get_one(slug)
|
recipe = self._get_recipe(slug)
|
||||||
|
|
||||||
if recipe is None:
|
if recipe is None:
|
||||||
raise exceptions.NoEntryFound("Recipe not found.")
|
raise exceptions.NoEntryFound("Recipe not found.")
|
||||||
@ -285,6 +286,11 @@ class RecipeService(BaseService):
|
|||||||
self.check_assets(new_data, recipe.slug)
|
self.check_assets(new_data, recipe.slug)
|
||||||
return new_data
|
return new_data
|
||||||
|
|
||||||
|
def update_last_made(self, slug: str, timestamp: datetime) -> Recipe:
|
||||||
|
# we bypass the pre update check since any user can update a recipe's last made date, even if it's locked
|
||||||
|
recipe = self._get_recipe(slug)
|
||||||
|
return self.repos.recipes.by_group(self.group.id).patch(recipe.slug, {"last_made": timestamp})
|
||||||
|
|
||||||
def delete_one(self, slug) -> Recipe:
|
def delete_one(self, slug) -> Recipe:
|
||||||
recipe = self._get_recipe(slug)
|
recipe = self._get_recipe(slug)
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from tests.utils import api_routes
|
from tests.utils import api_routes
|
||||||
@ -86,6 +88,36 @@ def test_user_locked_recipe(api_client: TestClient, user_tuple: list[TestUser])
|
|||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_update_last_made(api_client: TestClient, user_tuple: list[TestUser]) -> None:
|
||||||
|
usr_1, usr_2 = user_tuple
|
||||||
|
|
||||||
|
# Setup Recipe
|
||||||
|
recipe_name = random_string()
|
||||||
|
response = api_client.post(api_routes.recipes, json={"name": recipe_name}, headers=usr_1.token)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
# Get Recipe
|
||||||
|
response = api_client.get(api_routes.recipes + f"/{recipe_name}", headers=usr_1.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
recipe = response.json()
|
||||||
|
|
||||||
|
# Lock Recipe
|
||||||
|
recipe["settings"]["locked"] = True
|
||||||
|
response = api_client.put(api_routes.recipes + f"/{recipe_name}", json=recipe, headers=usr_1.token)
|
||||||
|
|
||||||
|
# User 2 should be able to update the last made timestamp
|
||||||
|
last_made_json = {"timestamp": datetime.now().isoformat()}
|
||||||
|
response = api_client.patch(
|
||||||
|
api_routes.recipes_slug_last_made(recipe_name), json=last_made_json, headers=usr_2.token
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
response = api_client.get(api_routes.recipes + f"/{recipe_name}", headers=usr_1.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
recipe = response.json()
|
||||||
|
assert recipe["lastMade"] == last_made_json["timestamp"]
|
||||||
|
|
||||||
|
|
||||||
def test_other_user_cant_lock_recipe(api_client: TestClient, user_tuple: list[TestUser]) -> None:
|
def test_other_user_cant_lock_recipe(api_client: TestClient, user_tuple: list[TestUser]) -> None:
|
||||||
usr_1, usr_2 = user_tuple
|
usr_1, usr_2 = user_tuple
|
||||||
|
|
||||||
|
@ -381,6 +381,11 @@ def recipes_slug_image(slug):
|
|||||||
return f"{prefix}/recipes/{slug}/image"
|
return f"{prefix}/recipes/{slug}/image"
|
||||||
|
|
||||||
|
|
||||||
|
def recipes_slug_last_made(slug):
|
||||||
|
"""`/api/recipes/{slug}/last-made`"""
|
||||||
|
return f"{prefix}/recipes/{slug}/last-made"
|
||||||
|
|
||||||
|
|
||||||
def recipes_slug_timeline_events(slug):
|
def recipes_slug_timeline_events(slug):
|
||||||
"""`/api/recipes/{slug}/timeline/events`"""
|
"""`/api/recipes/{slug}/timeline/events`"""
|
||||||
return f"{prefix}/recipes/{slug}/timeline/events"
|
return f"{prefix}/recipes/{slug}/timeline/events"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user