diff --git a/backend/sql/version_1.sql b/backend/sql/version_1.sql index 17c432e..61d72a5 100644 --- a/backend/sql/version_1.sql +++ b/backend/sql/version_1.sql @@ -17,6 +17,8 @@ CREATE TABLE [User] ( [default_servings] INTEGER DEFAULT 4, [lang] TEXT NOT NULL DEFAULT 'en', + -- 0: Monday, 1: Tuesday, 2: Wednesday, 3: Thursday, 4: Friday, 5: Saturday, 6: Sunday. + [first_day_of_the_week] INTEGER DEFAULT 0, [password] TEXT NOT NULL, -- argon2(password_plain, salt). @@ -34,6 +36,17 @@ CREATE TABLE [User] ( CREATE INDEX [validation_token_index] ON [User]([validation_token]); CREATE UNIQUE INDEX [User_email_index] ON [User]([email]); +CREATE TRIGGER [User_trigger_update_first_day_of_the_week] +BEFORE UPDATE OF [first_day_of_the_week] +ON [User] +BEGIN + SELECT + CASE + WHEN NEW.[first_day_of_the_week] < 0 OR NEW.[first_day_of_the_week] > 6 THEN + RAISE (ABORT, 'Invalid [first_day_of_the_week] value') + END; +END; + CREATE TABLE [UserLoginToken] ( [id] INTEGER PRIMARY KEY, [user_id] INTEGER NOT NULL, diff --git a/backend/src/data/db/user.rs b/backend/src/data/db/user.rs index 51c71f4..bde7fe8 100644 --- a/backend/src/data/db/user.rs +++ b/backend/src/data/db/user.rs @@ -77,7 +77,7 @@ FROM [UserLoginToken] WHERE [token] = $1 pub async fn load_user(&self, user_id: i64) -> Result> { sqlx::query_as( - "SELECT [id], [email], [name], [default_servings], [lang], [is_admin] FROM [User] WHERE [id] = $1", + "SELECT [id], [email], [name], [default_servings], [first_day_of_the_week], [lang], [is_admin] FROM [User] WHERE [id] = $1", ) .bind(user_id) .fetch_optional(&self.pool) @@ -93,16 +93,17 @@ FROM [UserLoginToken] WHERE [token] = $1 new_email: Option<&str>, new_name: Option<&str>, new_default_servings: Option, + new_first_day_of_the_week: Option, new_password: Option<&str>, ) -> Result { let mut tx = self.tx().await?; let hashed_new_password = new_password.map(|p| hash(p).unwrap()); - let (email, name, default_servings, hashed_password) = sqlx::query_as::< + let (email, name, default_servings, first_day_of_the_week, hashed_password) = sqlx::query_as::< _, - (String, String, u32, String), + (String, String, u32, u8, String), >( - "SELECT [email], [name], [default_servings], [password] FROM [User] WHERE [id] = $1", + "SELECT [email], [name], [default_servings], [first_day_of_the_week], [password] FROM [User] WHERE [id] = $1", ) .bind(user_id) .fetch_one(&mut *tx) @@ -148,7 +149,7 @@ WHERE [id] = $1 sqlx::query( r#" UPDATE [User] -SET [email] = $2, [name] = $3, [default_servings] = $4, [password] = $5 +SET [email] = $2, [name] = $3, [default_servings] = $4, [first_day_of_the_week] = $5, [password] = $6 WHERE [id] = $1 "#, ) @@ -156,6 +157,7 @@ WHERE [id] = $1 .bind(new_email.unwrap_or(&email)) .bind(new_name.map(str::trim).unwrap_or(&name)) .bind(new_default_servings.unwrap_or(default_servings)) + .bind(new_first_day_of_the_week.map(|d| d as u8).unwrap_or(first_day_of_the_week)) .bind(hashed_new_password.unwrap_or(hashed_password)) .execute(&mut *tx) .await?; @@ -179,8 +181,13 @@ WHERE [id] = $1 .map_err(DBError::from) } - pub async fn sign_up(&self, email: &str, password: &str) -> Result { - self.sign_up_with_given_time(email, password, Utc::now()) + pub async fn sign_up( + &self, + email: &str, + password: &str, + first_day_of_the_week: Weekday, + ) -> Result { + self.sign_up_with_given_time(email, password, first_day_of_the_week, Utc::now()) .await } @@ -188,6 +195,7 @@ WHERE [id] = $1 &self, email: &str, password: &str, + first_day_of_the_week: Weekday, datetime: DateTime, ) -> Result { let mut tx = self.tx().await?; @@ -229,11 +237,12 @@ WHERE [id] = $1 sqlx::query( r#" INSERT INTO [User] -([email], [creation_datetime], [validation_token], [validation_token_datetime], [password]) -VALUES ($1, $2, $3, $4, $5) +([email], [first_day_of_the_week], [creation_datetime], [validation_token], [validation_token_datetime], [password]) +VALUES ($1, $2, $3, $4, $5, $6) "#, ) .bind(email) + .bind(first_day_of_the_week as u8) .bind(Utc::now()) .bind(&token) .bind(datetime) @@ -525,7 +534,10 @@ mod tests { #[tokio::test] async fn sign_up() -> Result<()> { let connection = Connection::new_in_memory().await?; - match connection.sign_up("paul@atreides.com", "12345").await? { + match connection + .sign_up("paul@atreides.com", "12345", Weekday::Mon) + .await? + { SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case. other => panic!("{:?}", other), } @@ -550,7 +562,10 @@ INSERT INTO NULL ); "#)).await?; - match connection.sign_up("paul@atreides.com", "12345").await? { + match connection + .sign_up("paul@atreides.com", "12345", Weekday::Mon) + .await? + { SignUpResult::UserAlreadyExists => (), // Nominal case. other => panic!("{:?}", other), } @@ -564,7 +579,7 @@ INSERT INTO let email = "paul@atreides.com"; let password = "12345"; - match connection.sign_up(email, password).await? { + match connection.sign_up(email, password, Weekday::Mon).await? { SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case. other => panic!("{:?}", other), } @@ -600,7 +615,10 @@ VALUES ( ) "# ).bind(token)).await?; - match connection.sign_up("paul@atreides.com", "12345").await? { + match connection + .sign_up("paul@atreides.com", "12345", Weekday::Mon) + .await? + { SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case. other => panic!("{:?}", other), } @@ -610,7 +628,10 @@ VALUES ( #[tokio::test] async fn sign_up_then_send_validation_at_time() -> Result<()> { let connection = Connection::new_in_memory().await?; - let validation_token = match connection.sign_up("paul@atreides.com", "12345").await? { + let validation_token = match connection + .sign_up("paul@atreides.com", "12345", Weekday::Mon) + .await? + { SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case. other => panic!("{:?}", other), }; @@ -633,7 +654,12 @@ VALUES ( async fn sign_up_then_send_validation_too_late() -> Result<()> { let connection = Connection::new_in_memory().await?; let validation_token = match connection - .sign_up_with_given_time("paul@atreides.com", "12345", Utc::now() - Duration::days(1)) + .sign_up_with_given_time( + "paul@atreides.com", + "12345", + Weekday::Mon, + Utc::now() - Duration::days(1), + ) .await? { SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case. @@ -657,7 +683,10 @@ VALUES ( #[tokio::test] async fn sign_up_then_send_validation_with_bad_token() -> Result<()> { let connection = Connection::new_in_memory().await?; - let _validation_token = match connection.sign_up("paul@atreides.com", "12345").await? { + let _validation_token = match connection + .sign_up("paul@atreides.com", "12345", Weekday::Mon) + .await? + { SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case. other => panic!("{:?}", other), }; @@ -685,7 +714,7 @@ VALUES ( let password = "12345"; // Sign up. - let validation_token = match connection.sign_up(email, password).await? { + let validation_token = match connection.sign_up(email, password, Weekday::Mon).await? { SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case. other => panic!("{:?}", other), }; @@ -724,7 +753,7 @@ VALUES ( let password = "12345"; // Sign up. - let validation_token = match connection.sign_up(email, password).await? { + let validation_token = match connection.sign_up(email, password, Weekday::Mon).await? { SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case. other => panic!("{:?}", other), }; @@ -778,7 +807,7 @@ VALUES ( let password = "12345"; // Sign up. - let validation_token = match connection.sign_up(email, password).await? { + let validation_token = match connection.sign_up(email, password, Weekday::Mon).await? { SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case. other => panic!("{:?}", other), }; @@ -855,7 +884,7 @@ VALUES ( let new_password = "54321"; // Sign up. - let validation_token = match connection.sign_up(email, password).await? { + let validation_token = match connection.sign_up(email, password, Weekday::Mon).await? { SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case. other => panic!("{:?}", other), }; @@ -946,6 +975,7 @@ VALUES Some("muaddib@fremen.com"), Some("muaddib"), None, + None, Some("Chani"), ) .await? diff --git a/backend/src/data/model.rs b/backend/src/data/model.rs index 4ba52f8..087bac0 100644 --- a/backend/src/data/model.rs +++ b/backend/src/data/model.rs @@ -8,6 +8,10 @@ pub struct User { pub name: String, pub email: String, pub default_servings: u32, + + #[sqlx(try_from = "u8")] + pub first_day_of_the_week: Weekday, + pub lang: String, pub is_admin: bool, } diff --git a/backend/src/main.rs b/backend/src/main.rs index 82496d9..d668b64 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -89,6 +89,16 @@ struct Context { dark_theme: bool, } +impl Context { + pub fn first_day_of_the_week(&self) -> Weekday { + if let Some(user) = &self.user { + user.first_day_of_the_week + } else { + self.tr.first_day_of_week() + } + } +} + // TODO: Should main returns 'Result'? #[tokio::main] async fn main() { diff --git a/backend/src/services/user.rs b/backend/src/services/user.rs index 82a7192..815dc16 100644 --- a/backend/src/services/user.rs +++ b/backend/src/services/user.rs @@ -141,7 +141,11 @@ pub async fn sign_up_post( } match connection - .sign_up(&form_data.email, &form_data.password_1) + .sign_up( + &form_data.email, + &form_data.password_1, + context.tr.first_day_of_week(), + ) .await { Ok(db::user::SignUpResult::UserAlreadyExists) => { @@ -691,6 +695,7 @@ pub struct EditUserForm { name: String, email: String, default_servings: u32, + first_day_of_the_week: chrono::Weekday, password_1: String, password_2: String, } @@ -713,6 +718,11 @@ pub async fn edit_user_post( Extension(context): Extension, Form(form_data): Form, ) -> Result { + event!( + Level::DEBUG, + "First day of the week: {:?}", + form_data.first_day_of_the_week + ); if let Some(ref user) = context.user { fn error_response( error: ProfileUpdateError, @@ -783,6 +793,7 @@ pub async fn edit_user_post( Some(email_trimmed), Some(&form_data.name), Some(form_data.default_servings), + Some(form_data.first_day_of_the_week), new_password, ) .await diff --git a/backend/src/translation.rs b/backend/src/translation.rs index 2ffac8d..7d330e3 100644 --- a/backend/src/translation.rs +++ b/backend/src/translation.rs @@ -1,5 +1,6 @@ use std::{borrow::Borrow, fs::File, sync::LazyLock}; +use chrono::Weekday; use common::utils; use ron::de::from_reader; use serde::Deserialize; @@ -80,6 +81,7 @@ pub enum Sentence { ProfileTitle, ProfileEmail, ProfileDefaultServings, + ProfileFirstDayOfWeek, ProfileNewPassword, ProfileFollowEmailTitle, ProfileFollowEmailLink, @@ -126,6 +128,13 @@ pub enum Sentence { RecipeEstimatedTimeMinAbbreviation, // Calendar. + CalendarMonday, + CalendarTuesday, + CalendarWednesday, + CalendarThursday, + CalendarFriday, + CalendarSaturday, + CalendarSunday, CalendarMondayAbbreviation, CalendarTuesdayAbbreviation, CalendarWednesdayAbbreviation, @@ -197,6 +206,13 @@ impl Tr { pub fn current_lang_and_territory_code(&self) -> String { format!("{}-{}", self.lang.code, self.lang.territory) } + + pub fn first_day_of_week(&self) -> Weekday { + match (self.lang.code.as_ref(), self.lang.territory.as_ref()) { + ("en", "US") => Weekday::Sun, + _ => Weekday::Mon, + } + } } // #[macro_export] diff --git a/backend/templates/base.html b/backend/templates/base.html index dd78f02..1552082 100644 --- a/backend/templates/base.html +++ b/backend/templates/base.html @@ -1,5 +1,7 @@ - + diff --git a/backend/templates/calendar.html b/backend/templates/calendar.html index e5e079e..1112ae5 100644 --- a/backend/templates/calendar.html +++ b/backend/templates/calendar.html @@ -26,7 +26,7 @@
    - {% for day in [ + {% let day_names = [ Sentence::CalendarMondayAbbreviation, Sentence::CalendarTuesdayAbbreviation, Sentence::CalendarWednesdayAbbreviation, @@ -34,9 +34,16 @@ Sentence::CalendarFridayAbbreviation, Sentence::CalendarSaturdayAbbreviation, Sentence::CalendarSundayAbbreviation, - ] %} -
  • {{ context.tr.t(*day) }}
  • + ] %} + + {% for i in 0..7 %} +
  • + {{ context.tr.t(*day_names[ + (i + context.first_day_of_the_week().num_days_from_monday() as usize) % 7 + ]) }} +
  • {% endfor %} +
    diff --git a/backend/templates/profile.html b/backend/templates/profile.html index dad1d96..46011c0 100644 --- a/backend/templates/profile.html +++ b/backend/templates/profile.html @@ -26,6 +26,14 @@ autocapitalize="none" autocomplete="email" autofocus="autofocus"> {{ message_email }} + + + + + + + {{ message_password }} + - - - - - - - {{ message_password }} + + diff --git a/backend/translation.ron b/backend/translation.ron index 39d1466..0745507 100644 --- a/backend/translation.ron +++ b/backend/translation.ron @@ -67,6 +67,7 @@ (ProfileTitle, "Profile"), (ProfileEmail, "Email (need to be revalidated if changed)"), (ProfileDefaultServings, "Default servings"), + (ProfileFirstDayOfWeek, "First day of the week"), (ProfileNewPassword, "New password (minimum {} characters)"), (ProfileFollowEmailTitle, "Cooking Recipes: Email validation"), (ProfileFollowEmailLink, "Follow this link to validate this email address, {}"), @@ -110,6 +111,13 @@ (RecipeSomeServings, "{} servings"), (RecipeEstimatedTimeMinAbbreviation, "min"), + (CalendarMonday, "Monday"), + (CalendarTuesday, "Tuesday"), + (CalendarWednesday, "Wednesday"), + (CalendarThursday, "Thursday"), + (CalendarFriday, "Friday"), + (CalendarSaturday, "Saturday"), + (CalendarSunday, "Sunday"), (CalendarMondayAbbreviation, "Mon"), (CalendarTuesdayAbbreviation, "Tue"), (CalendarWednesdayAbbreviation, "Wed"), @@ -207,6 +215,7 @@ (ProfileTitle, "Profile"), (ProfileEmail, "Email (doit être revalidé si changé)"), (ProfileDefaultServings, "Nombre de portions par défaut"), + (ProfileFirstDayOfWeek, "Premier jour de la semaine"), (ProfileNewPassword, "Nouveau mot de passe (minimum {} caractères)"), (ProfileFollowEmailTitle, "Recettes de Cuisine: Validation de l'adresse email"), (ProfileFollowEmailLink, "Suivez ce lien pour valider l'adresse email, {}"), @@ -250,6 +259,13 @@ (RecipeSomeServings, "pour {} personnes"), (RecipeEstimatedTimeMinAbbreviation, "min"), + (CalendarMonday, "lundi"), + (CalendarTuesday, "mardi"), + (CalendarWednesday, "mercredi"), + (CalendarThursday, "jeudi"), + (CalendarFriday, "vendredi"), + (CalendarSaturday, "samedi"), + (CalendarSunday, "dimanche"), (CalendarMondayAbbreviation, "Lun"), (CalendarTuesdayAbbreviation, "Mar"), (CalendarWednesdayAbbreviation, "Mer"), diff --git a/common/src/ron_api.rs b/common/src/ron_api.rs index 62a25a9..87ca32c 100644 --- a/common/src/ron_api.rs +++ b/common/src/ron_api.rs @@ -63,7 +63,8 @@ pub struct SetRecipeEstimatedTime { pub estimated_time: Option, } -#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] +#[repr(u32)] +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Debug)] pub enum Difficulty { Unknown = 0, Easy = 1, diff --git a/frontend/src/calendar.rs b/frontend/src/calendar.rs index 4b5a1ef..aa18e4e 100644 --- a/frontend/src/calendar.rs +++ b/frontend/src/calendar.rs @@ -66,6 +66,7 @@ impl CalendarState { pub struct CalendarOptions { pub can_select_date: bool, pub with_link_and_remove: bool, + pub first_day_of_the_week: Weekday, } pub fn setup( @@ -118,7 +119,10 @@ pub fn setup( // gloo::console::log!(event); // TODO: Remove. if target.class_name() == "number" && options.can_select_date { - let first_day = first_grid_day(state_clone.get_displayed_date()); + let first_day = first_grid_day( + state_clone.get_displayed_date(), + options.first_day_of_the_week, + ); let day_grid_id = target.parent_element().unwrap().id(); let day_offset = day_grid_id[9..10].parse::().unwrap() * 7 + day_grid_id[10..11].parse::().unwrap(); @@ -212,7 +216,7 @@ fn display_month( } } - let first_day = first_grid_day(date); + let first_day = first_grid_day(date, options.first_day_of_the_week); let mut current = first_day; for i in 0..NB_CALENDAR_ROW { @@ -302,11 +306,11 @@ fn display_month( }); } -fn first_grid_day(mut date: NaiveDate) -> NaiveDate { +fn first_grid_day(mut date: NaiveDate, first_day_of_the_week: Weekday) -> NaiveDate { while (date - Days::new(1)).month() == date.month() { date = date - Days::new(1); } - while date.weekday() != Weekday::Mon { + while date.weekday() != first_day_of_the_week { date = date - Days::new(1); } date diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index b67f618..230b40a 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -44,6 +44,12 @@ pub fn main() -> Result<(), JsValue> { .map(|v| v == "true") .unwrap_or_default(); + let first_day_of_the_week = selector::("html") + .dataset() + .get("userFirstDayOfTheWeek") + .map(|v| v.parse().unwrap_or(chrono::Weekday::Mon)) + .unwrap_or(chrono::Weekday::Mon); + match path[..] { ["recipe", "edit", id] => { let id = id.parse::().unwrap(); // TODO: remove unwrap. @@ -51,11 +57,11 @@ pub fn main() -> Result<(), JsValue> { } ["recipe", "view", id] => { let id = id.parse::().unwrap(); // TODO: remove unwrap. - pages::recipe_view::setup_page(id, is_user_logged) + pages::recipe_view::setup_page(id, is_user_logged, first_day_of_the_week) } ["dev_panel"] => pages::dev_panel::setup_page(), // Home. - [""] => pages::home::setup_page(is_user_logged), + [""] => pages::home::setup_page(is_user_logged, first_day_of_the_week), _ => log!("Path unknown: ", location), } diff --git a/frontend/src/pages/home.rs b/frontend/src/pages/home.rs index 4dabbd2..a4db1ca 100644 --- a/frontend/src/pages/home.rs +++ b/frontend/src/pages/home.rs @@ -1,3 +1,4 @@ +use chrono::Weekday; use gloo::events::EventListener; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; @@ -10,7 +11,7 @@ use crate::{ utils::{SelectorExt, by_id, get_current_lang, get_locale, selector}, }; -pub fn setup_page(is_user_logged: bool) { +pub fn setup_page(is_user_logged: bool, first_day_of_the_week: Weekday) { let recipe_scheduler = RecipeScheduler::new(!is_user_logged); calendar::setup( @@ -18,6 +19,7 @@ pub fn setup_page(is_user_logged: bool) { calendar::CalendarOptions { can_select_date: false, with_link_and_remove: true, + first_day_of_the_week, }, recipe_scheduler, ); diff --git a/frontend/src/pages/recipe_view.rs b/frontend/src/pages/recipe_view.rs index 899f924..6158c64 100644 --- a/frontend/src/pages/recipe_view.rs +++ b/frontend/src/pages/recipe_view.rs @@ -1,7 +1,8 @@ +use chrono::Weekday; use common::utils::substitute_with_names; use gloo::events::EventListener; use wasm_bindgen_futures::spawn_local; -use web_sys::{Element, HtmlInputElement}; +use web_sys::{Element, HtmlElement, HtmlInputElement}; use crate::{ calendar, modal_dialog, @@ -10,7 +11,7 @@ use crate::{ utils::{SelectorExt, get_locale, selector}, }; -pub fn setup_page(recipe_id: i64, is_user_logged: bool) { +pub fn setup_page(recipe_id: i64, is_user_logged: bool, first_day_of_the_week: Weekday) { let recipe_scheduler = RecipeScheduler::new(!is_user_logged); let add_to_planner: Element = selector("#recipe-view .add-to-planner"); @@ -26,6 +27,7 @@ pub fn setup_page(recipe_id: i64, is_user_logged: bool) { calendar::CalendarOptions { can_select_date: true, with_link_and_remove: false, + first_day_of_the_week, }, recipe_scheduler, )