-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.
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)
.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(
) -> 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)
#[cfg(test)]
mod tests {
use super::*;
+ use chrono::Days;
#[tokio::test]
async fn create_a_new_recipe_then_update_its_title() -> Result<()> {
]
);
+ // 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?;
1,
Some("muaddib@fremen.com"),
Some("muaddib"),
+ None,
Some("Chani"),
)
.await?
)
}
+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,
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;
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";
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]
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?
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]
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]
) -> 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]
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,
}
}
+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]
CalendarDecember,
CalendarAddToPlanner,
CalendarAddToPlannerSuccess,
+ CalendarAddToPlannerAlreadyExists,
CalendarDateFormat, // See https://docs.rs/chrono/latest/chrono/format/strftime/index.html.
}
{% 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>
(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.
]
),
(
(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
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,
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::*;
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!"
+ );
+ }
}
-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> {
)
.await
{
- if request::post::<(), _>(
+ if let Ok(result) = request::post::<ron_api::ScheduleRecipeResult, _>(
"calendar/schedule_recipe",
ron_api::ScheduleRecipe {
recipe_id,
},
)
.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(),
],
));
},
+use std::str::FromStr;
+
+use chrono::Locale;
use gloo::utils::document;
use wasm_bindgen::prelude::*;
use web_sys::Element;
.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()
+}