[Database] Add 'creation_datetime' to User + some little things

This commit is contained in:
Greg Burri 2025-01-07 23:55:16 +01:00
parent 91ab379718
commit 7a09e2360e
14 changed files with 179 additions and 131 deletions

View file

@ -1,18 +1,20 @@
INSERT INTO [User] ([id], [email], [name], [password], [validation_token_datetime], [validation_token]) INSERT INTO [User] ([id], [email], [name], [creation_datetime], [password], [validation_token_datetime], [validation_token])
VALUES ( VALUES (
1, 1,
'paul@atreides.com', 'paul@atreides.com',
'Paul', 'Paul',
'2025-01-07T10:41:05.697884837+00:00',
'$argon2id$v=19$m=4096,t=4,p=2$l1fAMRc0VfkNzqpEfFEReg$/gsUsY2aML8EbKjPeCxucenxkxhiFSXDmizWZPLvNuo', '$argon2id$v=19$m=4096,t=4,p=2$l1fAMRc0VfkNzqpEfFEReg$/gsUsY2aML8EbKjPeCxucenxkxhiFSXDmizWZPLvNuo',
0, 0,
NULL NULL
); );
INSERT INTO [User] ([id], [email], [name], [password], [validation_token_datetime], [validation_token]) INSERT INTO [User] ([id], [email], [name], [creation_datetime], [password], [validation_token_datetime], [validation_token])
VALUES ( VALUES (
2, 2,
'alia@atreides.com', 'alia@atreides.com',
'Alia', 'Alia',
'2025-01-07T10:41:05.697884837+00:00',
'$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY', '$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
0, 0,
NULL NULL

View file

@ -7,8 +7,11 @@ CREATE TABLE [Version] (
CREATE TABLE [User] ( CREATE TABLE [User] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
[email] TEXT NOT NULL, [email] TEXT NOT NULL,
[name] TEXT NOT NULL DEFAULT '', [name] TEXT NOT NULL DEFAULT '',
[creation_datetime] TEXT NOT NULL,
[default_servings] INTEGER DEFAULT 4, [default_servings] INTEGER DEFAULT 4,
[lang] TEXT NOT NULL DEFAULT 'en', [lang] TEXT NOT NULL DEFAULT 'en',

View file

@ -628,14 +628,15 @@ mod tests {
sqlx::query( sqlx::query(
r#" r#"
INSERT INTO [User] INSERT INTO [User]
([id], [email], [name], [password], [validation_token_datetime], [validation_token]) ([id], [email], [name], [creation_datetime], [password], [validation_token_datetime], [validation_token])
VALUES VALUES
($1, $2, $3, $4, $5, $6) ($1, $2, $3, $4, $5, $6, $7)
"# "#
) )
.bind(user_id) .bind(user_id)
.bind("paul@atreides.com") .bind("paul@atreides.com")
.bind("paul") .bind("paul")
.bind("")
.bind("$argon2id$v=19$m=4096,t=3,p=1$G4fjepS05MkRbTqEImUdYg$GGziE8uVQe1L1oFHk37lBno10g4VISnVqynSkLCH3Lc") .bind("$argon2id$v=19$m=4096,t=3,p=1$G4fjepS05MkRbTqEImUdYg$GGziE8uVQe1L1oFHk37lBno10g4VISnVqynSkLCH3Lc")
.bind("2022-11-29 22:05:04.121407300+00:00") .bind("2022-11-29 22:05:04.121407300+00:00")
.bind(None::<&str>) // 'null'. .bind(None::<&str>) // 'null'.

View file

@ -222,11 +222,12 @@ WHERE [id] = $1
sqlx::query( sqlx::query(
r#" r#"
INSERT INTO [User] INSERT INTO [User]
([email], [validation_token], [validation_token_datetime], [password]) ([email], [creation_datetime], [validation_token], [validation_token_datetime], [password])
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4, $5)
"#, "#,
) )
.bind(email) .bind(email)
.bind(Utc::now())
.bind(&token) .bind(&token)
.bind(datetime) .bind(datetime)
.bind(hashed_password) .bind(hashed_password)
@ -509,11 +510,12 @@ mod tests {
sqlx::query( sqlx::query(
r#" r#"
INSERT INTO INSERT INTO
[User] ([id], [email], [name], [password], [validation_token_datetime], [validation_token]) [User] ([id], [email], [name], [creation_datetime], [password], [validation_token_datetime], [validation_token])
VALUES ( VALUES (
1, 1,
'paul@atreides.com', 'paul@atreides.com',
'paul', 'paul',
'',
'$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY', '$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
0, 0,
NULL NULL
@ -557,10 +559,11 @@ INSERT INTO
sqlx::query( sqlx::query(
r#" r#"
INSERT INTO [User] INSERT INTO [User]
([id], [email], [name], [password], [validation_token_datetime], [validation_token]) ([id], [email], [creation_datetime], [name], [password], [validation_token_datetime], [validation_token])
VALUES ( VALUES (
1, 1,
'paul@atreides.com', 'paul@atreides.com',
'',
'paul', 'paul',
'$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY', '$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
0, 0,
@ -896,17 +899,11 @@ VALUES (
sqlx::query( sqlx::query(
r#" r#"
INSERT INTO [User] INSERT INTO [User]
([id], [email], [name], [password], [validation_token_datetime], [validation_token]) ([id], [email], [name], [creation_datetime], [password], [validation_token_datetime], [validation_token])
VALUES VALUES
($1, $2, $3, $4, $5, $6) (1, 'paul@atreides.com', 'paul', '', '$argon2id$v=19$m=4096,t=3,p=1$G4fjepS05MkRbTqEImUdYg$GGziE8uVQe1L1oFHk37lBno10g4VISnVqynSkLCH3Lc', '2022-11-29 22:05:04.121407300+00:00', NULL)
"# "#
) )
.bind(1)
.bind("paul@atreides.com")
.bind("paul")
.bind("$argon2id$v=19$m=4096,t=3,p=1$G4fjepS05MkRbTqEImUdYg$GGziE8uVQe1L1oFHk37lBno10g4VISnVqynSkLCH3Lc")
.bind("2022-11-29 22:05:04.121407300+00:00")
.bind(None::<&str>) // 'null'.
).await?; ).await?;
let user = connection.load_user(1).await?.unwrap(); let user = connection.load_user(1).await?.unwrap();

View file

@ -28,16 +28,16 @@ pub struct HomeTemplate {
#[derive(Template)] #[derive(Template)]
#[template(path = "message.html")] #[template(path = "message.html")]
pub struct MessageTemplate { pub struct MessageTemplate<'a> {
pub user: Option<model::User>, pub user: Option<model::User>,
pub tr: Tr, pub tr: Tr,
pub message: String, pub message: &'a str,
pub as_code: bool, // Display the message in <pre> markup. pub as_code: bool, // Display the message in <pre> markup.
} }
impl MessageTemplate { impl<'a> MessageTemplate<'a> {
pub fn new(message: String, tr: Tr) -> MessageTemplate { pub fn new(message: &'a str, tr: Tr) -> MessageTemplate<'a> {
MessageTemplate { MessageTemplate {
user: None, user: None,
tr, tr,
@ -46,7 +46,11 @@ impl MessageTemplate {
} }
} }
pub fn new_with_user(message: String, tr: Tr, user: Option<model::User>) -> MessageTemplate { pub fn new_with_user(
message: &'a str,
tr: Tr,
user: Option<model::User>,
) -> MessageTemplate<'a> {
MessageTemplate { MessageTemplate {
user, user,
tr, tr,
@ -58,59 +62,59 @@ impl MessageTemplate {
#[derive(Template)] #[derive(Template)]
#[template(path = "sign_up_form.html")] #[template(path = "sign_up_form.html")]
pub struct SignUpFormTemplate { pub struct SignUpFormTemplate<'a> {
pub user: Option<model::User>, pub user: Option<model::User>,
pub tr: Tr, pub tr: Tr,
pub email: String, pub email: String,
pub message: String, pub message: &'a str,
pub message_email: String, pub message_email: &'a str,
pub message_password: String, pub message_password: &'a str,
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "sign_in_form.html")] #[template(path = "sign_in_form.html")]
pub struct SignInFormTemplate { pub struct SignInFormTemplate<'a> {
pub user: Option<model::User>, pub user: Option<model::User>,
pub tr: Tr, pub tr: Tr,
pub email: String, pub email: &'a str,
pub message: String, pub message: &'a str,
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "ask_reset_password.html")] #[template(path = "ask_reset_password.html")]
pub struct AskResetPasswordTemplate { pub struct AskResetPasswordTemplate<'a> {
pub user: Option<model::User>, pub user: Option<model::User>,
pub tr: Tr, pub tr: Tr,
pub email: String, pub email: &'a str,
pub message: String, pub message: &'a str,
pub message_email: String, pub message_email: &'a str,
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "reset_password.html")] #[template(path = "reset_password.html")]
pub struct ResetPasswordTemplate { pub struct ResetPasswordTemplate<'a> {
pub user: Option<model::User>, pub user: Option<model::User>,
pub tr: Tr, pub tr: Tr,
pub reset_token: String, pub reset_token: &'a str,
pub message: String, pub message: &'a str,
pub message_password: String, pub message_password: &'a str,
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "profile.html")] #[template(path = "profile.html")]
pub struct ProfileTemplate { pub struct ProfileTemplate<'a> {
pub user: Option<model::User>, pub user: Option<model::User>,
pub tr: Tr, pub tr: Tr,
pub username: String, pub username: &'a str,
pub email: String, pub email: &'a str,
pub message: String, pub message: &'a str,
pub message_email: String, pub message_email: &'a str,
pub message_password: String, pub message_password: &'a str,
} }
#[derive(Template)] #[derive(Template)]

View file

@ -245,7 +245,11 @@ async fn translation(
.map(|l| l.split('-').next().unwrap_or_default()) .map(|l| l.split('-').next().unwrap_or_default())
.find_or_first(|l| available_codes.contains(l)); .find_or_first(|l| available_codes.contains(l));
accept_language.unwrap_or("en").to_string() match accept_language {
Some(lang) if !lang.is_empty() => lang,
_ => translation::DEFAULT_LANGUAGE_CODE,
}
.to_string()
} }
} }
}; };

View file

@ -33,7 +33,7 @@ pub async fn ron_error_to_html(
}; };
return Ok(MessageTemplate { return Ok(MessageTemplate {
user: None, user: None,
message, message: &message,
as_code: true, as_code: true,
tr, tr,
} }
@ -78,6 +78,6 @@ pub async fn not_found(
) -> impl IntoResponse { ) -> impl IntoResponse {
( (
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
MessageTemplate::new_with_user("404: Not found".to_string(), tr, user), MessageTemplate::new_with_user("404: Not found", tr, user),
) )
} }

View file

@ -79,7 +79,7 @@ pub async fn view(
&& (user.is_none() || recipe.user_id != user.as_ref().unwrap().id) && (user.is_none() || recipe.user_id != user.as_ref().unwrap().id)
{ {
return Ok(MessageTemplate::new_with_user( return Ok(MessageTemplate::new_with_user(
tr.tp(Sentence::RecipeNotAllowedToView, &[Box::new(recipe_id)]), &tr.tp(Sentence::RecipeNotAllowedToView, &[Box::new(recipe_id)]),
tr, tr,
user, user,
) )

View file

@ -34,9 +34,9 @@ pub async fn sign_up_get(
user, user,
tr, tr,
email: String::new(), email: String::new(),
message: String::new(), message: "",
message_email: String::new(), message_email: "",
message_password: String::new(), message_password: "",
}) })
} }
@ -71,26 +71,27 @@ pub async fn sign_up_post(
user: Option<model::User>, user: Option<model::User>,
tr: translation::Tr, tr: translation::Tr,
) -> Result<Response> { ) -> Result<Response> {
let invalid_password_mess = &tr.tp(
Sentence::InvalidPassword,
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
);
Ok(SignUpFormTemplate { Ok(SignUpFormTemplate {
user, user,
email: form_data.email.clone(), email: form_data.email.clone(),
message_email: match error { message_email: match error {
SignUpError::InvalidEmail => tr.t(Sentence::InvalidEmail), SignUpError::InvalidEmail => tr.t(Sentence::InvalidEmail),
_ => String::new(), _ => "",
}, },
message_password: match error { message_password: match error {
SignUpError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch), SignUpError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
SignUpError::InvalidPassword => tr.tp( SignUpError::InvalidPassword => invalid_password_mess,
Sentence::InvalidPassword, _ => "",
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
),
_ => String::new(),
}, },
message: match error { message: match error {
SignUpError::UserAlreadyExists => tr.t(Sentence::EmailAlreadyTaken), SignUpError::UserAlreadyExists => tr.t(Sentence::EmailAlreadyTaken),
SignUpError::DatabaseError => "Database error".to_string(), SignUpError::DatabaseError => tr.t(Sentence::DatabaseError),
SignUpError::UnableSendEmail => tr.t(Sentence::UnableToSendEmail), SignUpError::UnableSendEmail => tr.t(Sentence::UnableToSendEmail),
_ => String::new(), _ => "",
}, },
tr, tr,
} }
@ -235,8 +236,8 @@ pub async fn sign_in_get(
Ok(SignInFormTemplate { Ok(SignInFormTemplate {
user, user,
tr, tr,
email: String::new(), email: "",
message: String::new(), message: "",
}) })
} }
@ -271,7 +272,7 @@ pub async fn sign_in_post(
jar, jar,
SignInFormTemplate { SignInFormTemplate {
user, user,
email: form_data.email, email: &form_data.email,
message: tr.t(Sentence::AccountMustBeValidatedFirst), message: tr.t(Sentence::AccountMustBeValidatedFirst),
tr, tr,
} }
@ -281,7 +282,7 @@ pub async fn sign_in_post(
jar, jar,
SignInFormTemplate { SignInFormTemplate {
user, user,
email: form_data.email, email: &form_data.email,
message: tr.t(Sentence::WrongEmailOrPassword), message: tr.t(Sentence::WrongEmailOrPassword),
tr, tr,
} }
@ -326,9 +327,9 @@ pub async fn ask_reset_password_get(
Ok(AskResetPasswordTemplate { Ok(AskResetPasswordTemplate {
user, user,
tr, tr,
email: String::new(), email: "",
message: String::new(), message: "",
message_email: String::new(), message_email: "",
} }
.into_response()) .into_response())
} }
@ -364,10 +365,10 @@ pub async fn ask_reset_password_post(
) -> Result<Response> { ) -> Result<Response> {
Ok(AskResetPasswordTemplate { Ok(AskResetPasswordTemplate {
user, user,
email: email.to_string(), email,
message_email: match error { message_email: match error {
AskResetPasswordError::InvalidEmail => tr.t(Sentence::InvalidEmail), AskResetPasswordError::InvalidEmail => tr.t(Sentence::InvalidEmail),
_ => String::new(), _ => "",
}, },
message: match error { message: match error {
AskResetPasswordError::EmailAlreadyReset => { AskResetPasswordError::EmailAlreadyReset => {
@ -376,7 +377,7 @@ pub async fn ask_reset_password_post(
AskResetPasswordError::EmailUnknown => tr.t(Sentence::EmailUnknown), AskResetPasswordError::EmailUnknown => tr.t(Sentence::EmailUnknown),
AskResetPasswordError::UnableSendEmail => tr.t(Sentence::UnableToSendResetEmail), AskResetPasswordError::UnableSendEmail => tr.t(Sentence::UnableToSendResetEmail),
AskResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError), AskResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError),
_ => String::new(), _ => "",
}, },
tr, tr,
} }
@ -470,9 +471,9 @@ pub async fn reset_password_get(
Ok(ResetPasswordTemplate { Ok(ResetPasswordTemplate {
user, user,
tr, tr,
reset_token: reset_token.to_string(), reset_token,
message: String::new(), message: "",
message_password: String::new(), message_password: "",
} }
.into_response()) .into_response())
} else { } else {
@ -510,21 +511,22 @@ pub async fn reset_password_post(
user: Option<model::User>, user: Option<model::User>,
tr: translation::Tr, tr: translation::Tr,
) -> Result<Response> { ) -> Result<Response> {
let reset_password_mess = &tr.tp(
Sentence::InvalidPassword,
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
);
Ok(ResetPasswordTemplate { Ok(ResetPasswordTemplate {
user, user,
reset_token: form_data.reset_token.clone(), reset_token: &form_data.reset_token,
message_password: match error { message_password: match error {
ResetPasswordError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch), ResetPasswordError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
ResetPasswordError::InvalidPassword => tr.tp( ResetPasswordError::InvalidPassword => reset_password_mess,
Sentence::InvalidPassword, _ => "",
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
),
_ => String::new(),
}, },
message: match error { message: match error {
ResetPasswordError::TokenExpired => tr.t(Sentence::AskResetTokenExpired), ResetPasswordError::TokenExpired => tr.t(Sentence::AskResetTokenExpired),
ResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError), ResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError),
_ => String::new(), _ => "",
}, },
tr, tr,
} }
@ -571,12 +573,12 @@ pub async fn edit_user_get(
) -> Response { ) -> Response {
if let Some(user) = user { if let Some(user) = user {
ProfileTemplate { ProfileTemplate {
username: user.name.clone(), username: &user.name,
email: user.email.clone(), email: &user.email,
message: String::new(), message: "",
message_email: String::new(), message_email: "",
message_password: String::new(), message_password: "",
user: Some(user), user: Some(user.clone()),
tr, tr,
} }
.into_response() .into_response()
@ -592,6 +594,7 @@ pub struct EditUserForm {
password_1: String, password_1: String,
password_2: String, password_2: String,
} }
enum ProfileUpdateError { enum ProfileUpdateError {
InvalidEmail, InvalidEmail,
EmailAlreadyTaken, EmailAlreadyTaken,
@ -618,27 +621,28 @@ pub async fn edit_user_post(
user: model::User, user: model::User,
tr: translation::Tr, tr: translation::Tr,
) -> Result<Response> { ) -> Result<Response> {
let invalid_password_mess = &tr.tp(
Sentence::InvalidPassword,
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
);
Ok(ProfileTemplate { Ok(ProfileTemplate {
user: Some(user), user: Some(user),
username: form_data.name.clone(), username: &form_data.name,
email: form_data.email.clone(), email: &form_data.email,
message_email: match error { message_email: match error {
ProfileUpdateError::InvalidEmail => tr.t(Sentence::InvalidEmail), ProfileUpdateError::InvalidEmail => tr.t(Sentence::InvalidEmail),
ProfileUpdateError::EmailAlreadyTaken => tr.t(Sentence::EmailAlreadyTaken), ProfileUpdateError::EmailAlreadyTaken => tr.t(Sentence::EmailAlreadyTaken),
_ => String::new(), _ => "",
}, },
message_password: match error { message_password: match error {
ProfileUpdateError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch), ProfileUpdateError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
ProfileUpdateError::InvalidPassword => tr.tp( ProfileUpdateError::InvalidPassword => invalid_password_mess,
Sentence::InvalidPassword, _ => "",
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
),
_ => String::new(),
}, },
message: match error { message: match error {
ProfileUpdateError::DatabaseError => tr.t(Sentence::DatabaseError), ProfileUpdateError::DatabaseError => tr.t(Sentence::DatabaseError),
ProfileUpdateError::UnableSendEmail => tr.t(Sentence::UnableToSendEmail), ProfileUpdateError::UnableSendEmail => tr.t(Sentence::UnableToSendEmail),
_ => String::new(), _ => "",
}, },
tr, tr,
} }
@ -666,7 +670,7 @@ pub async fn edit_user_post(
}; };
let email_trimmed = form_data.email.trim(); let email_trimmed = form_data.email.trim();
let message: String; let message: &str;
match connection match connection
.update_user( .update_user(
@ -725,11 +729,11 @@ pub async fn edit_user_post(
Ok(ProfileTemplate { Ok(ProfileTemplate {
user, user,
username: form_data.name, username: &form_data.name,
email: form_data.email, email: &form_data.email,
message, message,
message_email: String::new(), message_email: "",
message_password: String::new(), message_password: "",
tr, tr,
} }
.into_response()) .into_response())

View file

@ -6,7 +6,7 @@ use strum::EnumCount;
use strum_macros::EnumCount; use strum_macros::EnumCount;
use tracing::{event, Level}; use tracing::{event, Level};
use crate::consts; use crate::{consts, utils};
#[derive(Debug, Clone, EnumCount, Deserialize)] #[derive(Debug, Clone, EnumCount, Deserialize)]
pub enum Sentence { pub enum Sentence {
@ -108,7 +108,8 @@ pub enum Sentence {
RecipeIngredientComment, RecipeIngredientComment,
} }
const DEFAULT_LANGUAGE_CODE: &str = "en"; pub const DEFAULT_LANGUAGE_CODE: &str = "en";
pub const PLACEHOLDER_SUBSTITUTE: &str = "{}";
#[derive(Clone)] #[derive(Clone)]
pub struct Tr { pub struct Tr {
@ -122,38 +123,21 @@ impl Tr {
} }
} }
pub fn t(&self, sentence: Sentence) -> String { pub fn t(&self, sentence: Sentence) -> &'static str {
//&'static str { self.lang.get(sentence)
self.lang.get(sentence).to_string()
// match self.lang.translation.get(&sentence) {
// Some(str) => str.clone(),
// None => format!(
// "Translation missing, lang: {}/{}, element: {:?}",
// self.lang.name, self.lang.code, sentence
// ),
// }
} }
pub fn tp(&self, sentence: Sentence, params: &[Box<dyn ToString + Send>]) -> String { pub fn tp(&self, sentence: Sentence, params: &[Box<dyn ToString + Send>]) -> String {
// match self.lang.translation.get(&sentence) {
// Some(str) => {
// let mut result = str.clone();
// for p in params {
// result = result.replacen("{}", &p.to_string(), 1);
// }
// result
// }
// None => format!(
// "Translation missing, lang: {}/{}, element: {:?}",
// self.lang.name, self.lang.code, sentence
// ),
// }
let text = self.lang.get(sentence); let text = self.lang.get(sentence);
let mut result = text.to_string(); let params_as_string: Vec<String> = params.iter().map(|p| p.to_string()).collect();
for p in params { utils::substitute(
result = result.replacen("{}", &p.to_string(), 1); text,
} PLACEHOLDER_SUBSTITUTE,
result &params_as_string
.iter()
.map(AsRef::as_ref)
.collect::<Vec<_>>(),
)
} }
pub fn current_lang_code(&self) -> &str { pub fn current_lang_code(&self) -> &str {
@ -191,7 +175,6 @@ struct Language {
impl Language { impl Language {
pub fn from_stored_language(stored_language: StoredLanguage) -> Self { pub fn from_stored_language(stored_language: StoredLanguage) -> Self {
println!("!!!!!!!!!!!! {:?}", &stored_language.code);
Self { Self {
code: stored_language.code, code: stored_language.code,
name: stored_language.name, name: stored_language.name,

View file

@ -39,3 +39,44 @@ pub fn get_url_from_host(host: &str) -> String {
host host
) )
} }
pub fn substitute(str: &str, pattern: &str, replacements: &[&str]) -> String {
let mut result = String::with_capacity(
(str.len() + replacements.iter().map(|s| s.len()).sum::<usize>())
.saturating_sub(pattern.len() * replacements.len()),
);
let mut i = 0;
for s in str.split(pattern) {
result.push_str(s);
if i < replacements.len() {
result.push_str(replacements[i]);
}
i += 1;
}
if i == 1 {
return str.to_string();
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_substitute() {
assert_eq!(substitute("", "", &[]), "");
assert_eq!(substitute("", "", &[""]), "");
assert_eq!(substitute("", "{}", &["a"]), "");
assert_eq!(substitute("a", "{}", &["b"]), "a");
assert_eq!(substitute("a{}", "{}", &["b"]), "ab");
assert_eq!(substitute("{}c", "{}", &["b"]), "bc");
assert_eq!(substitute("a{}c", "{}", &["b"]), "abc");
assert_eq!(substitute("{}b{}", "{}", &["a", "c"]), "abc");
assert_eq!(substitute("{}{}{}", "{}", &["a", "bc", "def"]), "abcdef");
assert_eq!(substitute("{}{}{}", "{}", &["a"]), "a");
}
}

View file

@ -34,7 +34,14 @@ async fn reload_recipes_list(current_recipe_id: i64) {
pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> {
// Title. // Title.
{ {
let title: HtmlInputElement = by_id("input-title"); let Some(title) = document().get_element_by_id("input-title") else {
return Err(JsValue::from_str("Unable to find 'input-title' element"));
};
let title: HtmlInputElement = title.dyn_into().unwrap();
// Check if the recipe has been loaded.
let mut current_title = title.value(); let mut current_title = title.value();
EventListener::new(&title.clone(), "blur", move |_event| { EventListener::new(&title.clone(), "blur", move |_event| {
if title.value() != current_title { if title.value() != current_title {

View file

@ -5,7 +5,7 @@ mod request;
mod toast; mod toast;
mod utils; mod utils;
use gloo::{events::EventListener, utils::window}; use gloo::{console::log, events::EventListener, utils::window};
use utils::by_id; use utils::by_id;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local; use wasm_bindgen_futures::spawn_local;
@ -22,7 +22,9 @@ pub fn main() -> Result<(), JsValue> {
if let ["recipe", "edit", id] = path[..] { if let ["recipe", "edit", id] = path[..] {
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap. let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
handles::recipe_edit(id)?; if let Err(error) = handles::recipe_edit(id) {
log!(error);
}
// Disable: user editing data are now submitted as classic form data. // Disable: user editing data are now submitted as classic form data.
// ["user", "edit"] => { // ["user", "edit"] => {