Recipe edit (WIP): forms to edit groups, steps and ingredients
This commit is contained in:
parent
dd05a673d9
commit
07b7ff425e
25 changed files with 881 additions and 203 deletions
|
|
@ -35,7 +35,7 @@ fn main() {
|
|||
}
|
||||
|
||||
let output = if exists_in_path("sass.bat") {
|
||||
run_sass(Command::new("cmd").args(&["/C", "sass.bat"]))
|
||||
run_sass(Command::new("cmd").args(["/C", "sass.bat"]))
|
||||
} else {
|
||||
run_sass(&mut Command::new("sass"))
|
||||
};
|
||||
|
|
|
|||
|
|
@ -93,6 +93,22 @@ body {
|
|||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.group {
|
||||
border: 0.1em solid white;
|
||||
}
|
||||
|
||||
.step {
|
||||
border: 0.1em solid white;
|
||||
}
|
||||
|
||||
.ingredient {
|
||||
border: 0.1em solid white;
|
||||
}
|
||||
|
||||
#hidden-templates {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
|
|
|
|||
|
|
@ -18,14 +18,61 @@ VALUES (
|
|||
NULL
|
||||
);
|
||||
|
||||
INSERT INTO [Recipe] ([user_id], [title], [is_published])
|
||||
VALUES (1, 'Croissant au jambon', true);
|
||||
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published])
|
||||
VALUES (1, 1, 'Croissant au jambon', true);
|
||||
|
||||
INSERT INTO [Recipe] ([user_id], [title], [is_published])
|
||||
VALUES (1, 'Gratin de thon aux olives', true);
|
||||
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published])
|
||||
VALUES (2, 1, 'Gratin de thon aux olives', true);
|
||||
|
||||
INSERT INTO [Recipe] ([user_id], [title], [is_published])
|
||||
VALUES (1, 'Saumon en croute', true);
|
||||
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published])
|
||||
VALUES (3, 1, 'Saumon en croute', true);
|
||||
|
||||
INSERT INTO [Recipe] ([user_id], [title], [is_published])
|
||||
VALUES (2, 'Ouiche lorraine', true);
|
||||
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published])
|
||||
VALUES (4, 2, 'Ouiche lorraine', true);
|
||||
|
||||
|
||||
-- Groups, steps and ingredients for 'Gratin de thon'.
|
||||
INSERT INTO [Group] ([id], [order], [recipe_id], [name], [comment])
|
||||
VALUES (1, 1, 2, "Fond du gratin", "");
|
||||
|
||||
INSERT INTO [Group] ([id], [order], [recipe_id], [name], [comment])
|
||||
VALUES (2, 2, 2, "Sauce", "");
|
||||
|
||||
|
||||
INSERT INTO [Step] ([id], [order], [group_id], [action])
|
||||
VALUES (1, 1, 1, "Égoutter et émietter dans un plat à gratting graissé");
|
||||
|
||||
INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (1, 1, "Thon en boîte", "", 240, "g");
|
||||
|
||||
|
||||
INSERT INTO [Step] ([id], [order], [group_id], [action])
|
||||
VALUES (2, 2, 1, "Saupoudrer");
|
||||
|
||||
INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (2, 2, "Sel à l'origan", "", 1, "c-à-c");
|
||||
|
||||
|
||||
INSERT INTO [Step] ([id], [order], [group_id], [action])
|
||||
VALUES (3, 3, 2, "Mélanger au fouet et verser sur le thon dans le plat");
|
||||
|
||||
INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (3, 3, "Concentré de tomate", "", 4, "c-à-s");
|
||||
|
||||
INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (4, 3, "Poivre", "", 0.25, "c-à-c");
|
||||
|
||||
INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (5, 3, "Herbe de Provence", "", 0.5, "c-à-c");
|
||||
|
||||
INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (6, 3, "Crème à café ou demi-crème", "", 2, "dl");
|
||||
|
||||
INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (7, 3, "Olives farcies coupées en deuxs", "", 50, "g");
|
||||
|
||||
|
||||
INSERT INTO [Group] ([id], [order], [recipe_id], [name], [comment])
|
||||
VALUES (3, 3, 2,
|
||||
"15 à 20 minutes de cuisson au four à 220 °C",
|
||||
"Servir avec du riz ou des patates robe des champs");
|
||||
|
|
@ -89,21 +89,12 @@ CREATE TABLE [Tag] (
|
|||
|
||||
CREATE UNIQUE INDEX [Tag_name_lang_index] ON [Tag] ([name], [lang]);
|
||||
|
||||
CREATE TABLE [Ingredient] (
|
||||
[id] INTEGER PRIMARY KEY,
|
||||
[name] TEXT NOT NULL,
|
||||
[comment] TEXT NOT NULL DEFAULT '',
|
||||
[quantity_value] INTEGER,
|
||||
[quantity_unit] TEXT NOT NULL DEFAULT '',
|
||||
[input_step_id] INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY([input_step_id]) REFERENCES [Step]([id]) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE [Group] (
|
||||
[id] INTEGER PRIMARY KEY,
|
||||
|
||||
[order] INTEGER NOT NULL DEFAULT 0,
|
||||
[recipe_id] INTEGER NOT NULL,
|
||||
|
||||
[name] TEXT NOT NULL DEFAULT '',
|
||||
[comment] TEXT NOT NULL DEFAULT '',
|
||||
|
||||
|
|
@ -114,15 +105,30 @@ CREATE INDEX [Group_order_index] ON [Group]([order]);
|
|||
|
||||
CREATE TABLE [Step] (
|
||||
[id] INTEGER PRIMARY KEY,
|
||||
|
||||
[order] INTEGER NOT NULL DEFAULT 0,
|
||||
[action] TEXT NOT NULL DEFAULT '',
|
||||
[group_id] INTEGER NOT NULL,
|
||||
|
||||
[action] TEXT NOT NULL DEFAULT '',
|
||||
|
||||
FOREIGN KEY(group_id) REFERENCES [Group](id) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
CREATE INDEX [Step_order_index] ON [Group]([order]);
|
||||
|
||||
CREATE TABLE [Ingredient] (
|
||||
[id] INTEGER PRIMARY KEY,
|
||||
|
||||
[step_id] INTEGER NOT NULL,
|
||||
|
||||
[name] TEXT NOT NULL,
|
||||
[comment] TEXT NOT NULL DEFAULT '',
|
||||
[quantity_value] REAL,
|
||||
[quantity_unit] TEXT NOT NULL DEFAULT '',
|
||||
|
||||
FOREIGN KEY([step_id]) REFERENCES [Step]([id]) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
-- CREATE TABLE [IntermediateSubstance] (
|
||||
-- [id] INTEGER PRIMARY KEY,
|
||||
-- [name] TEXT NOT NULL DEFAULT '',
|
||||
|
|
|
|||
|
|
@ -41,22 +41,31 @@ impl fmt::Debug for Config {
|
|||
|
||||
pub fn load() -> Config {
|
||||
match File::open(consts::FILE_CONF) {
|
||||
Ok(file) => from_reader(file).expect(&format!(
|
||||
"Failed to open configuration file {}",
|
||||
consts::FILE_CONF
|
||||
)),
|
||||
Ok(file) => from_reader(file).unwrap_or_else(|error| {
|
||||
panic!(
|
||||
"Failed to open configuration file {}: {}",
|
||||
consts::FILE_CONF,
|
||||
error
|
||||
)
|
||||
}),
|
||||
Err(_) => {
|
||||
let file = File::create(consts::FILE_CONF).expect(&format!(
|
||||
"Failed to create default configuration file {}",
|
||||
consts::FILE_CONF
|
||||
));
|
||||
let file = File::create(consts::FILE_CONF).unwrap_or_else(|error| {
|
||||
panic!(
|
||||
"Failed to create default configuration file {}: {}",
|
||||
consts::FILE_CONF,
|
||||
error
|
||||
)
|
||||
});
|
||||
|
||||
let default_config = Config::default();
|
||||
|
||||
to_writer_pretty(file, &default_config, PrettyConfig::new()).expect(&format!(
|
||||
"Failed to write default configuration file {}",
|
||||
consts::FILE_CONF
|
||||
));
|
||||
to_writer_pretty(file, &default_config, PrettyConfig::new()).unwrap_or_else(|error| {
|
||||
panic!(
|
||||
"Failed to write default configuration file {}: {}",
|
||||
consts::FILE_CONF,
|
||||
error
|
||||
)
|
||||
});
|
||||
|
||||
default_config
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ pub const FILE_CONF: &str = "conf.ron";
|
|||
pub const DB_DIRECTORY: &str = "data";
|
||||
pub const DB_FILENAME: &str = "recipes.sqlite";
|
||||
pub const SQL_FILENAME: &str = "sql/version_{VERSION}.sql";
|
||||
pub const VALIDATION_TOKEN_DURATION: i64 = 1 * 60 * 60; // [s]. (1 jour).
|
||||
pub const VALIDATION_TOKEN_DURATION: i64 = 60 * 60; // [s]. (1 jour).
|
||||
pub const COOKIE_AUTH_TOKEN_NAME: &str = "auth_token";
|
||||
|
||||
pub const VALIDATION_PASSWORD_RESET_TOKEN_DURATION: i64 = 1 * 60 * 60; // [s]. (1 jour).
|
||||
pub const VALIDATION_PASSWORD_RESET_TOKEN_DURATION: i64 = 60 * 60; // [s]. (1 jour).
|
||||
|
||||
// Number of alphanumeric characters for tokens
|
||||
// (cookie authentication, password reset, validation token).
|
||||
|
|
@ -21,4 +21,4 @@ pub const REVERSE_PROXY_IP_HTTP_FIELD: &str = "x-real-ip"; // Set by the reverse
|
|||
|
||||
pub const MAX_DB_CONNECTION: u32 = 1024;
|
||||
|
||||
pub const LANGUAGES: [(&'static str, &'static str); 2] = [("Français", "fr"), ("English", "en")];
|
||||
pub const LANGUAGES: [(&str, &str); 2] = [("Français", "fr"), ("English", "en")];
|
||||
|
|
|
|||
|
|
@ -196,26 +196,10 @@ WHERE [type] = 'table' AND [name] = 'Version'
|
|||
}
|
||||
|
||||
fn load_sql_file<P: AsRef<Path> + fmt::Display>(sql_file: P) -> Result<String> {
|
||||
let mut file = File::open(&sql_file).map_err(|err| {
|
||||
DBError::Other(format!(
|
||||
"Cannot open SQL file ({}): {}",
|
||||
&sql_file,
|
||||
err.to_string()
|
||||
))
|
||||
})?;
|
||||
let mut file = File::open(&sql_file)
|
||||
.map_err(|err| DBError::Other(format!("Cannot open SQL file ({}): {}", &sql_file, err)))?;
|
||||
let mut sql = String::new();
|
||||
file.read_to_string(&mut sql).map_err(|err| {
|
||||
DBError::Other(format!(
|
||||
"Cannot read SQL file ({}) : {}",
|
||||
&sql_file,
|
||||
err.to_string()
|
||||
))
|
||||
})?;
|
||||
file.read_to_string(&mut sql)
|
||||
.map_err(|err| DBError::Other(format!("Cannot read SQL file ({}) : {}", &sql_file, err)))?;
|
||||
Ok(sql)
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -45,6 +45,21 @@ ORDER BY [title]
|
|||
.map_err(DBError::from)
|
||||
}
|
||||
|
||||
pub async fn can_edit_recipe_group(&self, user_id: i64, group_id: i64) -> Result<bool> {
|
||||
sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT COUNT(*)
|
||||
FROM [Recipe] INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
|
||||
WHERE [Group].[id] = $1 AND [user_id] = $2
|
||||
"#,
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(user_id)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(DBError::from)
|
||||
}
|
||||
|
||||
pub async fn get_recipe(&self, id: i64) -> Result<Option<model::Recipe>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
|
|
@ -166,6 +181,88 @@ WHERE [Recipe].[user_id] = $1
|
|||
.map(|_| ())
|
||||
.map_err(DBError::from)
|
||||
}
|
||||
|
||||
pub async fn get_groups(&self, recipe_id: i64) -> Result<Vec<model::Group>> {
|
||||
let mut tx = self.tx().await?;
|
||||
let mut groups: Vec<model::Group> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT [id], [name], [comment]
|
||||
FROM [Group]
|
||||
WHERE [recipe_id] = $1
|
||||
ORDER BY [order]
|
||||
"#,
|
||||
)
|
||||
.bind(recipe_id)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for group in groups.iter_mut() {
|
||||
group.steps = sqlx::query_as(
|
||||
r#"
|
||||
SELECT [id], [action]
|
||||
FROM [Step]
|
||||
WHERE [group_id] = $1
|
||||
ORDER BY [order]
|
||||
"#,
|
||||
)
|
||||
.bind(group.id)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for step in group.steps.iter_mut() {
|
||||
step.ingredients = sqlx::query_as(
|
||||
r#"
|
||||
SELECT [id], [name], [comment], [quantity_value], [quantity_unit]
|
||||
FROM [Ingredient]
|
||||
WHERE [step_id] = $1
|
||||
ORDER BY [name]
|
||||
"#,
|
||||
)
|
||||
.bind(step.id)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
pub async fn add_recipe_group(&self, recipe_id: i64) -> Result<i64> {
|
||||
let db_result = sqlx::query("INSERT INTO [Group] ([recipe_id]) VALUES ($1)")
|
||||
.bind(recipe_id)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(db_result.last_insert_rowid())
|
||||
}
|
||||
|
||||
pub async fn rm_recipe_group(&self, group_id: i64) -> Result<()> {
|
||||
sqlx::query("DELETE FROM [Group] WHERE [id] = $1")
|
||||
.bind(group_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(DBError::from)
|
||||
}
|
||||
|
||||
pub async fn set_group_name(&self, group_id: i64, name: &str) -> Result<()> {
|
||||
sqlx::query("UPDATE [Group] SET [name] = $2 WHERE [id] = $1")
|
||||
.bind(group_id)
|
||||
.bind(name)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(DBError::from)
|
||||
}
|
||||
|
||||
pub async fn set_group_comment(&self, group_id: i64, comment: &str) -> Result<()> {
|
||||
sqlx::query("UPDATE [Group] SET [comment] = $2 WHERE [id] = $1")
|
||||
.bind(group_id)
|
||||
.bind(comment)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(DBError::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -214,7 +311,7 @@ mod tests {
|
|||
assert_eq!(recipe.estimated_time, Some(420));
|
||||
assert_eq!(recipe.difficulty, Difficulty::Medium);
|
||||
assert_eq!(recipe.lang, "fr");
|
||||
assert_eq!(recipe.is_published, true);
|
||||
assert!(recipe.is_published);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ FROM [User] WHERE [email] = $1
|
|||
return Ok(SignUpResult::UserAlreadyExists);
|
||||
}
|
||||
let token = generate_token();
|
||||
let hashed_password = hash(password).map_err(|e| DBError::from_dyn_error(e))?;
|
||||
let hashed_password = hash(password).map_err(DBError::from_dyn_error)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE [User]
|
||||
|
|
@ -208,7 +208,7 @@ WHERE [id] = $1
|
|||
}
|
||||
None => {
|
||||
let token = generate_token();
|
||||
let hashed_password = hash(password).map_err(|e| DBError::from_dyn_error(e))?;
|
||||
let hashed_password = hash(password).map_err(DBError::from_dyn_error)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO [User]
|
||||
|
|
@ -336,19 +336,18 @@ WHERE [id] = $1
|
|||
|
||||
pub async fn sign_out(&self, token: &str) -> Result<()> {
|
||||
let mut tx = self.tx().await?;
|
||||
match sqlx::query_scalar::<_, i64>("SELECT [id] FROM [UserLoginToken] WHERE [token] = $1")
|
||||
.bind(token)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
|
||||
if let Some(login_id) =
|
||||
sqlx::query_scalar::<_, i64>("SELECT [id] FROM [UserLoginToken] WHERE [token] = $1")
|
||||
.bind(token)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
{
|
||||
Some(login_id) => {
|
||||
sqlx::query("DELETE FROM [UserLoginToken] WHERE [id] = $1")
|
||||
.bind(login_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
}
|
||||
None => (),
|
||||
sqlx::query("DELETE FROM [UserLoginToken] WHERE [id] = $1")
|
||||
.bind(login_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -429,7 +428,7 @@ WHERE [password_reset_token] = $1
|
|||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let hashed_new_password = hash(new_password).map_err(|e| DBError::from_dyn_error(e))?;
|
||||
let hashed_new_password = hash(new_password).map_err(DBError::from_dyn_error)?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
|
|
@ -853,7 +852,7 @@ VALUES (
|
|||
};
|
||||
|
||||
connection
|
||||
.reset_password(&new_password, &token, Duration::hours(1))
|
||||
.reset_password(new_password, &token, Duration::hours(1))
|
||||
.await?;
|
||||
|
||||
// Sign in.
|
||||
|
|
|
|||
|
|
@ -34,20 +34,30 @@ pub struct Recipe {
|
|||
// pub groups: Vec<Group>,
|
||||
}
|
||||
|
||||
#[derive(FromRow)]
|
||||
pub struct Group {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub comment: String,
|
||||
|
||||
#[sqlx(skip)]
|
||||
pub steps: Vec<Step>,
|
||||
}
|
||||
|
||||
#[derive(FromRow)]
|
||||
pub struct Step {
|
||||
pub id: i64,
|
||||
pub action: String,
|
||||
|
||||
#[sqlx(skip)]
|
||||
pub ingredients: Vec<Ingredient>,
|
||||
}
|
||||
|
||||
#[derive(FromRow)]
|
||||
pub struct Ingredient {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub comment: String,
|
||||
pub quantity: i32,
|
||||
pub quantity_value: f64,
|
||||
pub quantity_unit: String,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,20 +9,20 @@ use crate::consts;
|
|||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum Error {
|
||||
ParseError(lettre::address::AddressError),
|
||||
SmtpError(lettre::transport::smtp::Error),
|
||||
Parse(lettre::address::AddressError),
|
||||
Smtp(lettre::transport::smtp::Error),
|
||||
Email(lettre::error::Error),
|
||||
}
|
||||
|
||||
impl From<lettre::address::AddressError> for Error {
|
||||
fn from(error: lettre::address::AddressError) -> Self {
|
||||
Error::ParseError(error)
|
||||
Error::Parse(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lettre::transport::smtp::Error> for Error {
|
||||
fn from(error: lettre::transport::smtp::Error) -> Self {
|
||||
Error::SmtpError(error)
|
||||
Error::Smtp(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use axum::{
|
|||
http::StatusCode,
|
||||
middleware::{self, Next},
|
||||
response::{Response, Result},
|
||||
routing::{get, put},
|
||||
routing::{delete, get, post, put},
|
||||
Router,
|
||||
};
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
|
|
@ -101,6 +101,14 @@ async fn main() {
|
|||
"/recipe/set_is_published",
|
||||
put(services::ron::set_is_published),
|
||||
)
|
||||
.route("/recipe/get_groups", get(services::ron::get_groups))
|
||||
.route("/recipe/add_group", post(services::ron::add_group))
|
||||
.route("/recipe/remove_group", delete(services::ron::rm_group))
|
||||
.route("/recipe/set_group_name", put(services::ron::set_group_name))
|
||||
.route(
|
||||
"/recipe/set_group_comment",
|
||||
put(services::ron::set_group_comment),
|
||||
)
|
||||
.fallback(services::ron::not_found);
|
||||
|
||||
let fragments_routes = Router::new().route(
|
||||
|
|
@ -183,7 +191,7 @@ async fn get_current_user(
|
|||
) -> Option<model::User> {
|
||||
match jar.get(consts::COOKIE_AUTH_TOKEN_NAME) {
|
||||
Some(token_cookie) => match connection
|
||||
.authentication(token_cookie.value(), &client_ip, &client_user_agent)
|
||||
.authentication(token_cookie.value(), client_ip, client_user_agent)
|
||||
.await
|
||||
{
|
||||
Ok(db::user::AuthenticationResult::NotValidToken) => None,
|
||||
|
|
@ -234,12 +242,15 @@ async fn process_args() -> bool {
|
|||
}
|
||||
})
|
||||
.unwrap();
|
||||
std::fs::copy(&db_path, &db_path_bckup).expect(&format!(
|
||||
"Unable to make backup of {:?} to {:?}",
|
||||
&db_path, &db_path_bckup
|
||||
));
|
||||
std::fs::remove_file(&db_path)
|
||||
.expect(&format!("Unable to remove db file: {:?}", &db_path));
|
||||
std::fs::copy(&db_path, &db_path_bckup).unwrap_or_else(|error| {
|
||||
panic!(
|
||||
"Unable to make backup of {:?} to {:?}: {}",
|
||||
&db_path, &db_path_bckup, error
|
||||
)
|
||||
});
|
||||
std::fs::remove_file(&db_path).unwrap_or_else(|error| {
|
||||
panic!("Unable to remove db file {:?}: {}", &db_path, error)
|
||||
});
|
||||
}
|
||||
|
||||
match db::Connection::new().await {
|
||||
|
|
|
|||
|
|
@ -60,10 +60,8 @@ where
|
|||
{
|
||||
match from_bytes::<T>(&body) {
|
||||
Ok(ron) => Ok(ron),
|
||||
Err(error) => {
|
||||
return Err(RonError {
|
||||
error: format!("Ron parsing error: {}", error),
|
||||
});
|
||||
}
|
||||
Err(error) => Err(RonError {
|
||||
error: format!("Ron parsing error: {}", error),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use axum::{
|
||||
body, debug_handler,
|
||||
extract::{Extension, Request, State},
|
||||
http::header,
|
||||
http::{header, StatusCode},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response, Result},
|
||||
};
|
||||
|
|
@ -66,5 +66,8 @@ pub async fn home_page(
|
|||
|
||||
#[debug_handler]
|
||||
pub async fn not_found(Extension(user): Extension<Option<model::User>>) -> impl IntoResponse {
|
||||
MessageTemplate::new_with_user("404: Not found", user)
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
MessageTemplate::new_with_user("404: Not found", user),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use axum::{
|
||||
debug_handler,
|
||||
extract::{Extension, State},
|
||||
extract::{Extension, Query, State},
|
||||
response::{IntoResponse, Result},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
// use tracing::{event, Level};
|
||||
|
||||
use crate::{
|
||||
|
|
@ -10,9 +11,15 @@ use crate::{
|
|||
html_templates::*,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CurrentRecipeId {
|
||||
current_recipe_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn recipes_list_fragments(
|
||||
State(connection): State<db::Connection>,
|
||||
current_recipe: Query<CurrentRecipeId>,
|
||||
Extension(user): Extension<Option<model::User>>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let recipes = Recipes {
|
||||
|
|
@ -24,8 +31,7 @@ pub async fn recipes_list_fragments(
|
|||
} else {
|
||||
vec![]
|
||||
},
|
||||
current_id: None,
|
||||
current_id: current_recipe.current_recipe_id,
|
||||
};
|
||||
|
||||
Ok(RecipesListFragmentTemplate { user, recipes })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,13 +48,19 @@
|
|||
|
||||
use axum::{
|
||||
debug_handler,
|
||||
extract::{Extension, State},
|
||||
extract::{Extension, Query, State},
|
||||
http::StatusCode,
|
||||
response::{ErrorResponse, IntoResponse, Result},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
// use tracing::{event, Level};
|
||||
|
||||
use crate::{data::db, model, ron_extractor::ExtractRon, ron_utils::ron_error};
|
||||
use crate::{
|
||||
data::db,
|
||||
model,
|
||||
ron_extractor::ExtractRon,
|
||||
ron_utils::{ron_error, ron_response},
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[debug_handler]
|
||||
|
|
@ -81,7 +87,7 @@ pub async fn update_user(
|
|||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
async fn check_user_rights(
|
||||
async fn check_user_rights_recipe(
|
||||
connection: &db::Connection,
|
||||
user: &Option<model::User>,
|
||||
recipe_id: i64,
|
||||
|
|
@ -100,13 +106,32 @@ async fn check_user_rights(
|
|||
}
|
||||
}
|
||||
|
||||
async fn check_user_rights_recipe_group(
|
||||
connection: &db::Connection,
|
||||
user: &Option<model::User>,
|
||||
group_id: i64,
|
||||
) -> Result<()> {
|
||||
if user.is_none()
|
||||
|| !connection
|
||||
.can_edit_recipe_group(user.as_ref().unwrap().id, group_id)
|
||||
.await?
|
||||
{
|
||||
Err(ErrorResponse::from(ron_error(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"Action not authorized",
|
||||
)))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn set_recipe_title(
|
||||
State(connection): State<db::Connection>,
|
||||
Extension(user): Extension<Option<model::User>>,
|
||||
ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeTitle>,
|
||||
) -> Result<StatusCode> {
|
||||
check_user_rights(&connection, &user, ron.recipe_id).await?;
|
||||
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
|
||||
connection
|
||||
.set_recipe_title(ron.recipe_id, &ron.title)
|
||||
.await?;
|
||||
|
|
@ -119,7 +144,7 @@ pub async fn set_recipe_description(
|
|||
Extension(user): Extension<Option<model::User>>,
|
||||
ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeDescription>,
|
||||
) -> Result<StatusCode> {
|
||||
check_user_rights(&connection, &user, ron.recipe_id).await?;
|
||||
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
|
||||
connection
|
||||
.set_recipe_description(ron.recipe_id, &ron.description)
|
||||
.await?;
|
||||
|
|
@ -132,7 +157,7 @@ pub async fn set_estimated_time(
|
|||
Extension(user): Extension<Option<model::User>>,
|
||||
ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeEstimatedTime>,
|
||||
) -> Result<StatusCode> {
|
||||
check_user_rights(&connection, &user, ron.recipe_id).await?;
|
||||
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
|
||||
connection
|
||||
.set_recipe_estimated_time(ron.recipe_id, ron.estimated_time)
|
||||
.await?;
|
||||
|
|
@ -145,7 +170,7 @@ pub async fn set_difficulty(
|
|||
Extension(user): Extension<Option<model::User>>,
|
||||
ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeDifficulty>,
|
||||
) -> Result<StatusCode> {
|
||||
check_user_rights(&connection, &user, ron.recipe_id).await?;
|
||||
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
|
||||
connection
|
||||
.set_recipe_difficulty(ron.recipe_id, ron.difficulty)
|
||||
.await?;
|
||||
|
|
@ -158,7 +183,7 @@ pub async fn set_language(
|
|||
Extension(user): Extension<Option<model::User>>,
|
||||
ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeLanguage>,
|
||||
) -> Result<StatusCode> {
|
||||
check_user_rights(&connection, &user, ron.recipe_id).await?;
|
||||
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
|
||||
connection
|
||||
.set_recipe_language(ron.recipe_id, &ron.lang)
|
||||
.await?;
|
||||
|
|
@ -171,13 +196,128 @@ pub async fn set_is_published(
|
|||
Extension(user): Extension<Option<model::User>>,
|
||||
ExtractRon(ron): ExtractRon<common::ron_api::SetIsPublished>,
|
||||
) -> Result<StatusCode> {
|
||||
check_user_rights(&connection, &user, ron.recipe_id).await?;
|
||||
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
|
||||
connection
|
||||
.set_recipe_is_published(ron.recipe_id, ron.is_published)
|
||||
.await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
impl From<model::Group> for common::ron_api::Group {
|
||||
fn from(group: model::Group) -> Self {
|
||||
Self {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
comment: group.comment,
|
||||
steps: group
|
||||
.steps
|
||||
.into_iter()
|
||||
.map(common::ron_api::Step::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<model::Step> for common::ron_api::Step {
|
||||
fn from(step: model::Step) -> Self {
|
||||
Self {
|
||||
id: step.id,
|
||||
action: step.action,
|
||||
ingredients: step
|
||||
.ingredients
|
||||
.into_iter()
|
||||
.map(common::ron_api::Ingredient::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<model::Ingredient> for common::ron_api::Ingredient {
|
||||
fn from(ingredient: model::Ingredient) -> Self {
|
||||
Self {
|
||||
id: ingredient.id,
|
||||
name: ingredient.name,
|
||||
comment: ingredient.comment,
|
||||
quantity_value: ingredient.quantity_value,
|
||||
quantity_unit: ingredient.quantity_unit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RecipeId {
|
||||
#[serde(rename = "recipe_id")]
|
||||
id: i64,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn get_groups(
|
||||
State(connection): State<db::Connection>,
|
||||
recipe_id: Query<RecipeId>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
println!("PROUT");
|
||||
// Here we don't check user rights on purpose.
|
||||
Ok(ron_response(
|
||||
StatusCode::OK,
|
||||
connection
|
||||
.get_groups(recipe_id.id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(common::ron_api::Group::from)
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn add_group(
|
||||
State(connection): State<db::Connection>,
|
||||
Extension(user): Extension<Option<model::User>>,
|
||||
ExtractRon(ron): ExtractRon<common::ron_api::AddRecipeGroup>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
|
||||
let group_id = connection.add_recipe_group(ron.recipe_id).await?;
|
||||
|
||||
Ok(ron_response(
|
||||
StatusCode::OK,
|
||||
common::ron_api::AddRecipeGroupResult { group_id },
|
||||
))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn rm_group(
|
||||
State(connection): State<db::Connection>,
|
||||
Extension(user): Extension<Option<model::User>>,
|
||||
ExtractRon(ron): ExtractRon<common::ron_api::RemoveRecipeGroup>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
check_user_rights_recipe_group(&connection, &user, ron.group_id).await?;
|
||||
connection.rm_recipe_group(ron.group_id).await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn set_group_name(
|
||||
State(connection): State<db::Connection>,
|
||||
Extension(user): Extension<Option<model::User>>,
|
||||
ExtractRon(ron): ExtractRon<common::ron_api::SetGroupName>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
check_user_rights_recipe_group(&connection, &user, ron.group_id).await?;
|
||||
connection.set_group_name(ron.group_id, &ron.name).await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn set_group_comment(
|
||||
State(connection): State<db::Connection>,
|
||||
Extension(user): Extension<Option<model::User>>,
|
||||
ExtractRon(ron): ExtractRon<common::ron_api::SetGroupComment>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
check_user_rights_recipe_group(&connection, &user, ron.group_id).await?;
|
||||
connection
|
||||
.set_group_comment(ron.group_id, &ron.comment)
|
||||
.await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
///// 404 /////
|
||||
#[debug_handler]
|
||||
pub async fn not_found(Extension(_user): Extension<Option<model::User>>) -> impl IntoResponse {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ use crate::{
|
|||
utils, AppState,
|
||||
};
|
||||
|
||||
//// SIGN UP /////
|
||||
/// SIGN UP ///
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn sign_up_get(
|
||||
|
|
@ -207,7 +207,7 @@ pub async fn sign_up_validation(
|
|||
}
|
||||
}
|
||||
|
||||
///// SIGN IN /////
|
||||
/// SIGN IN ///
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn sign_in_get(
|
||||
|
|
@ -271,7 +271,7 @@ pub async fn sign_in_post(
|
|||
}
|
||||
}
|
||||
|
||||
///// SIGN OUT /////
|
||||
/// SIGN OUT ///
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn sign_out(
|
||||
|
|
@ -287,7 +287,7 @@ pub async fn sign_out(
|
|||
Ok((jar, Redirect::to("/")))
|
||||
}
|
||||
|
||||
///// RESET PASSWORD /////
|
||||
/// RESET PASSWORD ///
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn ask_reset_password_get(
|
||||
|
|
@ -510,7 +510,7 @@ pub async fn reset_password_post(
|
|||
}
|
||||
}
|
||||
|
||||
///// EDIT PROFILE /////
|
||||
/// EDIT PROFILE ///
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn edit_user_get(Extension(user): Extension<Option<model::User>>) -> Response {
|
||||
|
|
@ -614,7 +614,7 @@ pub async fn edit_user_post(
|
|||
match connection
|
||||
.update_user(
|
||||
user.id,
|
||||
Some(&email_trimmed),
|
||||
Some(email_trimmed),
|
||||
Some(&form_data.name),
|
||||
new_password,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,35 +13,27 @@
|
|||
<input
|
||||
id="input-title"
|
||||
type="text"
|
||||
name="title"
|
||||
value="{{ recipe.title }}"
|
||||
autocomplete="title"
|
||||
autofocus="true" />
|
||||
|
||||
<label for="input-description">Description</label>
|
||||
<input
|
||||
id="input-description"
|
||||
type="text"
|
||||
name="description"
|
||||
value="{{ recipe.description }}"
|
||||
autocomplete="title" />
|
||||
<label for="text-area-description">Description</label>
|
||||
<textarea
|
||||
id="text-area-description">{{ recipe.description }}</textarea>
|
||||
|
||||
<label for="input-description">Estimated time</label>
|
||||
<label for="input-estimated-time">Estimated time</label>
|
||||
<input
|
||||
id="input-estimated-time"
|
||||
type="number"
|
||||
name="estimated-time"
|
||||
value="
|
||||
{% match recipe.estimated_time %}
|
||||
{% when Some with (t) %}
|
||||
{{ t }}
|
||||
{% when None %}
|
||||
0
|
||||
{% endmatch %}"
|
||||
autocomplete="title" />
|
||||
{% endmatch %}"/>
|
||||
|
||||
<label for="select-difficulty">Difficulty</label>
|
||||
<select id="select-difficulty" name="difficulty">
|
||||
<select id="select-difficulty">
|
||||
<option value="0" {%+ call is_difficulty(common::ron_api::Difficulty::Unknown) %}> - </option>
|
||||
<option value="1" {%+ call is_difficulty(common::ron_api::Difficulty::Easy) %}>Easy</option>
|
||||
<option value="2" {%+ call is_difficulty(common::ron_api::Difficulty::Medium) %}>Medium</option>
|
||||
|
|
@ -49,16 +41,19 @@
|
|||
</select>
|
||||
|
||||
<label for="select-language">Language</label>
|
||||
<select id="select-language" name="language">
|
||||
<select id="select-language">
|
||||
{% for lang in languages %}
|
||||
<option value="{{ lang.1 }}">{{ lang.0 }}</option>
|
||||
<option value="{{ lang.1 }}"
|
||||
{%+ if recipe.lang == lang.1 %}
|
||||
selected
|
||||
{% endif %}
|
||||
>{{ lang.0 }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<input
|
||||
id="input-is-published"
|
||||
type="checkbox"
|
||||
name="is-published"
|
||||
{%+ if recipe.is_published %}
|
||||
checked
|
||||
{% endif %}
|
||||
|
|
@ -66,6 +61,42 @@
|
|||
<label for="input-is-published">Is published</label>
|
||||
|
||||
<div id="groups-container">
|
||||
|
||||
</div>
|
||||
<input id="button-add-group" type="button" value="Add a group"/>
|
||||
|
||||
<div id="hidden-templates">
|
||||
<div class="group">
|
||||
<label for="input-group-name">Name</label>
|
||||
<input class="input-group-name" type="text" />
|
||||
|
||||
<label for="input-group-comment">Comment</label>
|
||||
<input class="input-group-comment" type="text" />
|
||||
|
||||
<div class="steps"></div>
|
||||
|
||||
<input class="button-add-step" type="button" value="Add a step"/>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<label for="text-area-step-action">Action</label>
|
||||
<textarea class="text-area-step-action"></textarea>
|
||||
|
||||
<div class="ingredients"></div>
|
||||
|
||||
<input class="button-add-ingedient" type="button" value="Add an ingredient"/>
|
||||
</div>
|
||||
|
||||
<div class="ingredient">
|
||||
<label for="input-ingredient-quantity">Quantity</label>
|
||||
<input class="input-ingredient-quantity" type="number" />
|
||||
|
||||
<label for="input-ingredient-unit">Unity</label>
|
||||
<input class="input-ingredient-unit" type="text" />
|
||||
|
||||
<label for="input-ingredient-name">Name</label>
|
||||
<input class="input-ingredient-name" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue