Toast message when scheduling a recipe
This commit is contained in:
parent
fbef990022
commit
ccb1248da3
11 changed files with 168 additions and 66 deletions
|
|
@ -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<AddScheduledRecipeResult> {
|
||||
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?;
|
||||
|
|
|
|||
|
|
@ -945,6 +945,7 @@ VALUES
|
|||
1,
|
||||
Some("muaddib@fremen.com"),
|
||||
Some("muaddib"),
|
||||
None,
|
||||
Some("Chani"),
|
||||
)
|
||||
.await?
|
||||
|
|
|
|||
|
|
@ -42,6 +42,13 @@ pub fn ron_error(status: StatusCode, message: &str) -> impl IntoResponse {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn ron_response_ok<T>(ron: T) -> impl IntoResponse
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
ron_response(StatusCode::OK, ron)
|
||||
}
|
||||
|
||||
pub fn ron_response<T>(status: StatusCode, ron: T) -> impl IntoResponse
|
||||
where
|
||||
T: Serialize,
|
||||
|
|
|
|||
|
|
@ -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<db::Connection>,
|
||||
recipe_id: Query<RecipeId>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
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<RecipeId>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
// 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<impl IntoResponse> {
|
||||
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<DateRange>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
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<data::db::recipe::AddScheduledRecipeResult> 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<db::Connection>,
|
||||
Extension(user): Extension<Option<model::User>>,
|
||||
ExtractRon(ron): ExtractRon<common::ron_api::ScheduleRecipe>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
) -> Result<Response> {
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ pub enum Sentence {
|
|||
CalendarDecember,
|
||||
CalendarAddToPlanner,
|
||||
CalendarAddToPlannerSuccess,
|
||||
CalendarAddToPlannerAlreadyExists,
|
||||
CalendarDateFormat, // See https://docs.rs/chrono/latest/chrono/format/strftime/index.html.
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@
|
|||
{% endif %}
|
||||
|
||||
<span class="calendar-add-to-planner-success">{{ tr.t(Sentence::CalendarAddToPlannerSuccess) }}</span>
|
||||
<span class="calendar-add-to-planner-already-exists">{{ tr.t(Sentence::CalendarAddToPlannerAlreadyExists) }}</span>
|
||||
<span class="calendar-date-format">{{ tr.t(Sentence::CalendarDateFormat) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
]
|
||||
)
|
||||
]
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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!"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<ron_api::ScheduleRecipeResult, _>(
|
||||
"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::<Element>("#recipe-view .recipe-title").inner_html();
|
||||
let date_format =
|
||||
selector::<Element>("#hidden-templates .calendar-date-format")
|
||||
.inner_html();
|
||||
let locale = {
|
||||
let lang_and_territory = selector::<Element>("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(),
|
||||
],
|
||||
));
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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::<T>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn get_locale() -> Locale {
|
||||
let lang_and_territory = selector::<Element>("html")
|
||||
.get_attribute("lang")
|
||||
.unwrap()
|
||||
.replace("-", "_");
|
||||
Locale::from_str(&lang_and_territory).unwrap_or_default()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue