From: Greg Burri Date: Wed, 5 Feb 2025 14:44:48 +0000 (+0100) Subject: Toast message when scheduling a recipe X-Git-Url: https://git.euphorik.ch/?a=commitdiff_plain;h=ccb1248da3f80305781ce7c166f2251bdb4e3b6a;p=recipes.git Toast message when scheduling a recipe --- diff --git a/backend/src/data/db/recipe.rs b/backend/src/data/db/recipe.rs index 4db47da..0776a84 100644 --- a/backend/src/data/db/recipe.rs +++ b/backend/src/data/db/recipe.rs @@ -1,9 +1,15 @@ -use chrono::{prelude::*, Days}; +use chrono::prelude::*; use common::ron_api::Difficulty; use itertools::Itertools; +use sqlx::Error; use super::{Connection, DBError, Result}; -use crate::{data::model, user_authentication}; +use crate::data::model; + +pub enum AddScheduledRecipeResult { + Ok, + RecipeAlreadyScheduledAtThisDate, +} impl Connection { /// Returns all the recipe titles where recipe is written in the given language. @@ -758,8 +764,8 @@ VALUES ($1, $2) recipe_id: i64, date: NaiveDate, servings: u32, - ) -> Result<()> { - sqlx::query( + ) -> Result { + match sqlx::query( r#" INSERT INTO [RecipeScheduled] (user_id, recipe_id, date, servings) VALUES ($1, $2, $3, $4) @@ -771,8 +777,15 @@ VALUES ($1, $2, $3, $4) .bind(servings) .execute(&self.pool) .await - .map(|_| ()) - .map_err(DBError::from) + { + Err(Error::Database(error)) + if error.code() == Some(std::borrow::Cow::Borrowed("2067")) + && error.message() == "UNIQUE constraint failed: RecipeScheduled.user_id, RecipeScheduled.recipe_id, RecipeScheduled.date" => + { + Ok(AddScheduledRecipeResult::RecipeAlreadyScheduledAtThisDate) + } + _ => Ok(AddScheduledRecipeResult::Ok), + } } pub async fn rm_scheduled_recipe( @@ -783,9 +796,9 @@ VALUES ($1, $2, $3, $4) ) -> Result<()> { sqlx::query( r#" -DELETE FROM [RecipeScheduled] -WHERE [user_id] = $1 AND [recipe_id] = $2 AND [date] = $3 - "#, + DELETE FROM [RecipeScheduled] + WHERE [user_id] = $1 AND [recipe_id] = $2 AND [date] = $3 + "#, ) .bind(user_id) .bind(recipe_id) @@ -823,6 +836,7 @@ ORDER BY [date] #[cfg(test)] mod tests { use super::*; + use chrono::Days; #[tokio::test] async fn create_a_new_recipe_then_update_its_title() -> Result<()> { @@ -1007,6 +1021,14 @@ VALUES ] ); + // Recipe scheduled at the same date is forbidden. + let Ok(AddScheduledRecipeResult::RecipeAlreadyScheduledAtThisDate) = connection + .add_scheduled_recipe(user_id, recipe_id_1, today, 4) + .await + else { + panic!("DBError::RecipeAlreadyScheduledAtThisDate must be returned"); + }; + connection .rm_scheduled_recipe(user_id, recipe_id_1, today) .await?; diff --git a/backend/src/data/db/user.rs b/backend/src/data/db/user.rs index f057d8c..cdfa76f 100644 --- a/backend/src/data/db/user.rs +++ b/backend/src/data/db/user.rs @@ -945,6 +945,7 @@ VALUES 1, Some("muaddib@fremen.com"), Some("muaddib"), + None, Some("Chani"), ) .await? diff --git a/backend/src/ron_utils.rs b/backend/src/ron_utils.rs index 4aa7d22..98dd03e 100644 --- a/backend/src/ron_utils.rs +++ b/backend/src/ron_utils.rs @@ -42,6 +42,13 @@ pub fn ron_error(status: StatusCode, message: &str) -> impl IntoResponse { ) } +pub fn ron_response_ok(ron: T) -> impl IntoResponse +where + T: Serialize, +{ + ron_response(StatusCode::OK, ron) +} + pub fn ron_response(status: StatusCode, ron: T) -> impl IntoResponse where T: Serialize, diff --git a/backend/src/services/ron.rs b/backend/src/services/ron.rs index cfc4650..192c65c 100644 --- a/backend/src/services/ron.rs +++ b/backend/src/services/ron.rs @@ -2,7 +2,7 @@ use axum::{ debug_handler, extract::{Extension, Query, State}, http::{HeaderMap, StatusCode}, - response::{ErrorResponse, IntoResponse, Result}, + response::{ErrorResponse, IntoResponse, Response, Result}, }; use axum_extra::extract::cookie::{Cookie, CookieJar}; use chrono::NaiveDate; @@ -11,10 +11,10 @@ use serde::Deserialize; use crate::{ consts, - data::db, + data::{self, db}, model, ron_extractor::ExtractRon, - ron_utils::{ron_error, ron_response}, + ron_utils::{ron_error, ron_response_ok}, }; const NOT_AUTHORIZED_MESSAGE: &str = "Action not authorized"; @@ -257,13 +257,10 @@ pub async fn get_tags( State(connection): State, recipe_id: Query, ) -> Result { - Ok(ron_response( - StatusCode::OK, - common::ron_api::Tags { - recipe_id: recipe_id.id, - tags: connection.get_recipes_tags(recipe_id.id).await?, - }, - )) + Ok(ron_response_ok(common::ron_api::Tags { + recipe_id: recipe_id.id, + tags: connection.get_recipes_tags(recipe_id.id).await?, + })) } #[debug_handler] @@ -401,8 +398,7 @@ pub async fn get_groups( recipe_id: Query, ) -> Result { // Here we don't check user rights on purpose. - Ok(ron_response( - StatusCode::OK, + Ok(ron_response_ok( connection .get_groups(recipe_id.id) .await? @@ -421,7 +417,7 @@ pub async fn add_group( check_user_rights_recipe(&connection, &user, ron.id).await?; let id = connection.add_recipe_group(ron.id).await?; - Ok(ron_response(StatusCode::OK, common::ron_api::Id { id })) + Ok(ron_response_ok(common::ron_api::Id { id })) } #[debug_handler] @@ -479,7 +475,7 @@ pub async fn add_step( check_user_rights_recipe_group(&connection, &user, ron.id).await?; let id = connection.add_recipe_step(ron.id).await?; - Ok(ron_response(StatusCode::OK, common::ron_api::Id { id })) + Ok(ron_response_ok(common::ron_api::Id { id })) } #[debug_handler] @@ -523,7 +519,7 @@ pub async fn add_ingredient( ) -> Result { check_user_rights_recipe_step(&connection, &user, ron.id).await?; let id = connection.add_recipe_ingredient(ron.id).await?; - Ok(ron_response(StatusCode::OK, common::ron_api::Id { id })) + Ok(ron_response_ok(common::ron_api::Id { id })) } #[debug_handler] @@ -615,14 +611,11 @@ pub async fn get_scheduled_recipes( date_range: Query, ) -> Result { if let Some(user) = user { - Ok(ron_response( - StatusCode::OK, - common::ron_api::ScheduledRecipes { - recipes: connection - .get_scheduled_recipes(user.id, date_range.start_date, date_range.end_date) - .await?, - }, - )) + Ok(ron_response_ok(common::ron_api::ScheduledRecipes { + recipes: connection + .get_scheduled_recipes(user.id, date_range.start_date, date_range.end_date) + .await?, + })) } else { Err(ErrorResponse::from(ron_error( StatusCode::UNAUTHORIZED, @@ -631,19 +624,35 @@ pub async fn get_scheduled_recipes( } } +impl From for common::ron_api::ScheduleRecipeResult { + fn from(db_res: data::db::recipe::AddScheduledRecipeResult) -> Self { + match db_res { + db::recipe::AddScheduledRecipeResult::Ok => Self::Ok, + db::recipe::AddScheduledRecipeResult::RecipeAlreadyScheduledAtThisDate => { + Self::RecipeAlreadyScheduledAtThisDate + } + } + } +} + #[debug_handler] pub async fn schedule_recipe( State(connection): State, Extension(user): Extension>, ExtractRon(ron): ExtractRon, -) -> Result { +) -> Result { check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; if let Some(user) = user { connection .add_scheduled_recipe(user.id, ron.recipe_id, ron.date, ron.servings) - .await?; + .await + .map(|res| { + ron_response_ok(common::ron_api::ScheduleRecipeResult::from(res)).into_response() + }) + .map_err(ErrorResponse::from) + } else { + Ok(StatusCode::OK.into_response()) } - Ok(StatusCode::OK) } #[debug_handler] diff --git a/backend/src/translation.rs b/backend/src/translation.rs index 189cf8c..472e83a 100644 --- a/backend/src/translation.rs +++ b/backend/src/translation.rs @@ -143,6 +143,7 @@ pub enum Sentence { CalendarDecember, CalendarAddToPlanner, CalendarAddToPlannerSuccess, + CalendarAddToPlannerAlreadyExists, CalendarDateFormat, // See https://docs.rs/chrono/latest/chrono/format/strftime/index.html. } diff --git a/backend/templates/recipe_view.html b/backend/templates/recipe_view.html index f32c8a3..1d899b9 100644 --- a/backend/templates/recipe_view.html +++ b/backend/templates/recipe_view.html @@ -94,6 +94,7 @@ {% endif %} {{ tr.t(Sentence::CalendarAddToPlannerSuccess) }} + {{ tr.t(Sentence::CalendarAddToPlannerAlreadyExists) }} {{ tr.t(Sentence::CalendarDateFormat) }} diff --git a/backend/translation.ron b/backend/translation.ron index 6b4d939..a24c95b 100644 --- a/backend/translation.ron +++ b/backend/translation.ron @@ -126,8 +126,9 @@ (CalendarNovember, "November"), (CalendarDecember, "December"), (CalendarAddToPlanner, "Add to planner"), - (CalendarAddToPlannerSuccess, "Recipe {} has been scheduled for {}"), - (CalendarDateFormat, "%A, %-d %B, %C%y"), + (CalendarAddToPlannerSuccess, "Recipe {title} has been scheduled for {date}"), + (CalendarAddToPlannerAlreadyExists, "Recipe {title} has already been scheduled for {date}"), + (CalendarDateFormat, "%A, %-d %B, %C%y"), // See https://docs.rs/chrono/latest/chrono/format/strftime/index.html. ] ), ( @@ -257,8 +258,9 @@ (CalendarNovember, "Novembre"), (CalendarDecember, "Décembre"), (CalendarAddToPlanner, "Ajouter au planificateur"), - (CalendarAddToPlannerSuccess, "La recette {} a été agendée pour le {}"), - (CalendarDateFormat, "%A %-d %B %C%y"), + (CalendarAddToPlannerSuccess, "La recette {title} a été agendée pour le {date}"), + (CalendarAddToPlannerAlreadyExists, "La recette {title} a été déjà été agendée pour le {date}"), + (CalendarDateFormat, "%A %-d %B %C%y"), // See https://docs.rs/chrono/latest/chrono/format/strftime/index.html. ] ) ] \ No newline at end of file diff --git a/common/src/ron_api.rs b/common/src/ron_api.rs index 189cabd..6b610ec 100644 --- a/common/src/ron_api.rs +++ b/common/src/ron_api.rs @@ -183,6 +183,12 @@ pub struct ScheduleRecipe { pub servings: u32, } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum ScheduleRecipeResult { + Ok, + RecipeAlreadyScheduledAtThisDate, +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ScheduledRecipe { pub recipe_id: i64, diff --git a/common/src/utils.rs b/common/src/utils.rs index 2efe599..344882b 100644 --- a/common/src/utils.rs +++ b/common/src/utils.rs @@ -35,6 +35,18 @@ pub fn substitute(str: &str, pattern: &str, replacements: &[&str]) -> String { result } +/// Example: substitute_with_names("{hello}, {world}!", &["{hello}", "{world"], &["Hello", "World"]) +pub fn substitute_with_names(str: &str, names: &[&str], replacements: &[&str]) -> String { + let mut result = str.to_string(); + for (i, name) in names.iter().enumerate() { + if i >= replacements.len() { + break; + } + result = result.replace(name, replacements[i]); + } + result +} + #[cfg(test)] mod tests { use super::*; @@ -52,4 +64,36 @@ mod tests { assert_eq!(substitute("{}{}{}", "{}", &["a", "bc", "def"]), "abcdef"); assert_eq!(substitute("{}{}{}", "{}", &["a"]), "a"); } + + #[test] + fn test_substitute_with_names() { + assert_eq!( + substitute_with_names("{hello}, {world}!", &["{hello}"], &["Hello", "World"]), + "Hello, {world}!" + ); + + assert_eq!( + substitute_with_names("{hello}, {world}!", &[], &["Hello", "World"]), + "{hello}, {world}!" + ); + + assert_eq!( + substitute_with_names("{hello}, {world}!", &["{hello}", "{world}"], &["Hello"]), + "Hello, {world}!" + ); + + assert_eq!( + substitute_with_names("{hello}, {world}!", &["{hello}", "{world}"], &[]), + "{hello}, {world}!" + ); + + assert_eq!( + substitute_with_names( + "{hello}, {world}!", + &["{hello}", "{world}"], + &["Hello", "World"] + ), + "Hello, World!" + ); + } } diff --git a/frontend/src/recipe_view.rs b/frontend/src/recipe_view.rs index 26b9699..de99be8 100644 --- a/frontend/src/recipe_view.rs +++ b/frontend/src/recipe_view.rs @@ -1,24 +1,16 @@ -use std::{cell::RefCell, future::Future, rc::Rc, str::FromStr}; +use std::str::FromStr; use chrono::Locale; -use common::{ron_api, utils::substitute}; -use gloo::{ - console::log, - events::EventListener, - net::http::Request, - utils::{document, window}, -}; +use common::{ron_api, utils::substitute_with_names}; +use gloo::events::EventListener; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; -use web_sys::{ - DragEvent, Element, HtmlDivElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement, - KeyboardEvent, -}; +use web_sys::{Element, HtmlInputElement}; use crate::{ calendar, modal_dialog, request, toast::{self, Level}, - utils::{by_id, selector, selector_and_clone, SelectorExt}, + utils::{get_locale, selector, SelectorExt}, }; pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> { @@ -38,7 +30,7 @@ pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> { ) .await { - if request::post::<(), _>( + if let Ok(result) = request::post::( "calendar/schedule_recipe", ron_api::ScheduleRecipe { recipe_id, @@ -47,31 +39,37 @@ pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> { }, ) .await - .is_ok() { toast::show_element_and_initialize( - Level::Success, - "#hidden-templates .calendar-add-to-planner-success", + match result { + ron_api::ScheduleRecipeResult::Ok => Level::Success, + ron_api::ScheduleRecipeResult::RecipeAlreadyScheduledAtThisDate => { + Level::Warning + } + }, + match result { + ron_api::ScheduleRecipeResult::Ok => { + "#hidden-templates .calendar-add-to-planner-success" + } + ron_api::ScheduleRecipeResult::RecipeAlreadyScheduledAtThisDate => { + "#hidden-templates .calendar-add-to-planner-already-exists" + } + }, |element| { let title = selector::("#recipe-view .recipe-title").inner_html(); let date_format = selector::("#hidden-templates .calendar-date-format") .inner_html(); - let locale = { - let lang_and_territory = selector::("html") - .get_attribute("lang") - .unwrap() - .replace("-", "_"); - Locale::from_str(&lang_and_territory).unwrap_or_default() - }; - element.set_inner_html(&substitute( + element.set_inner_html(&substitute_with_names( &element.inner_html(), - "{}", + &["{title}", "{date}"], &[ &title, - &date.format_localized(&date_format, locale).to_string(), + &date + .format_localized(&date_format, get_locale()) + .to_string(), ], )); }, diff --git a/frontend/src/utils.rs b/frontend/src/utils.rs index 6e400e0..4732ad3 100644 --- a/frontend/src/utils.rs +++ b/frontend/src/utils.rs @@ -1,3 +1,6 @@ +use std::str::FromStr; + +use chrono::Locale; use gloo::utils::document; use wasm_bindgen::prelude::*; use web_sys::Element; @@ -86,3 +89,11 @@ where .dyn_into::() .unwrap() } + +pub fn get_locale() -> Locale { + let lang_and_territory = selector::("html") + .get_attribute("lang") + .unwrap() + .replace("-", "_"); + Locale::from_str(&lang_and_territory).unwrap_or_default() +}