Recipe edit (WIP): all form fields are now saved

This commit is contained in:
Greg Burri 2024-12-27 00:39:23 +01:00
parent 07b7ff425e
commit 6876a254e1
12 changed files with 563 additions and 210 deletions

View file

@ -53,7 +53,7 @@ body {
font-family: Fira Code, Helvetica Neue, Helvetica, Arial, sans-serif;
text-shadow: 2px 2px 2px rgb(0, 0, 0);
// line-height: 18px;
color: rgb(255, 255, 255);
color: lighten($primary, 60%);
background-color: $background;
margin: 0px;
@ -63,7 +63,7 @@ body {
.recipe-item-current {
padding: 3px;
border: 1px solid white;
border: 1px solid lighten($primary, 30%);
}
.header-container {
@ -87,7 +87,7 @@ body {
flex-grow: 1;
background-color: $background-container;
border: 0.1em solid white;
border: 0.1em solid lighten($primary, 50%);
padding: 0.5em;
h1 {
@ -95,15 +95,15 @@ body {
}
.group {
border: 0.1em solid white;
border: 0.1em solid lighten($primary, 30%);
}
.step {
border: 0.1em solid white;
border: 0.1em solid lighten($primary, 30%);
}
.ingredient {
border: 0.1em solid white;
border: 0.1em solid lighten($primary, 30%);
}
#hidden-templates {

View file

@ -49,7 +49,8 @@ ORDER BY [title]
sqlx::query_scalar(
r#"
SELECT COUNT(*)
FROM [Recipe] INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
FROM [Recipe]
INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
WHERE [Group].[id] = $1 AND [user_id] = $2
"#,
)
@ -60,6 +61,45 @@ WHERE [Group].[id] = $1 AND [user_id] = $2
.map_err(DBError::from)
}
pub async fn can_edit_recipe_step(&self, user_id: i64, step_id: i64) -> Result<bool> {
sqlx::query_scalar(
r#"
SELECT COUNT(*)
FROM [Recipe]
INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
INNER JOIN [Step] ON [Step].[group_id] = [Group].[id]
WHERE [Step].[id] = $1 AND [user_id] = $2
"#,
)
.bind(step_id)
.bind(user_id)
.fetch_one(&self.pool)
.await
.map_err(DBError::from)
}
pub async fn can_edit_recipe_ingredient(
&self,
user_id: i64,
ingredient_id: i64,
) -> Result<bool> {
sqlx::query_scalar(
r#"
SELECT COUNT(*)
FROM [Recipe]
INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
INNER JOIN [Step] ON [Step].[group_id] = [Group].[id]
INNER JOIN [Ingredient] ON [Ingredient].[step_id] = [Step].[id]
WHERE [Ingredient].[id] = $1 AND [user_id] = $2
"#,
)
.bind(ingredient_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#"
@ -263,6 +303,60 @@ ORDER BY [name]
.map(|_| ())
.map_err(DBError::from)
}
pub async fn set_step_action(&self, step_id: i64, action: &str) -> Result<()> {
sqlx::query("UPDATE [Step] SET [action] = $2 WHERE [id] = $1")
.bind(step_id)
.bind(action)
.execute(&self.pool)
.await
.map(|_| ())
.map_err(DBError::from)
}
pub async fn set_ingredient_name(&self, ingredient_id: i64, name: &str) -> Result<()> {
sqlx::query("UPDATE [Ingredient] SET [name] = $2 WHERE [id] = $1")
.bind(ingredient_id)
.bind(name)
.execute(&self.pool)
.await
.map(|_| ())
.map_err(DBError::from)
}
pub async fn set_ingredient_comment(&self, ingredient_id: i64, comment: &str) -> Result<()> {
sqlx::query("UPDATE [Ingredient] SET [comment] = $2 WHERE [id] = $1")
.bind(ingredient_id)
.bind(comment)
.execute(&self.pool)
.await
.map(|_| ())
.map_err(DBError::from)
}
pub async fn set_ingredient_quantity(
&self,
ingredient_id: i64,
quantity: Option<f64>,
) -> Result<()> {
sqlx::query("UPDATE [Ingredient] SET [quantity_value] = $2 WHERE [id] = $1")
.bind(ingredient_id)
.bind(quantity)
.execute(&self.pool)
.await
.map(|_| ())
.map_err(DBError::from)
}
pub async fn set_ingredient_unit(&self, ingredient_id: i64, unit: &str) -> Result<()> {
sqlx::query("UPDATE [Ingredient] SET [quantity_unit] = $2 WHERE [id] = $1")
.bind(ingredient_id)
.bind(unit)
.execute(&self.pool)
.await
.map(|_| ())
.map_err(DBError::from)
}
}
#[cfg(test)]

View file

@ -61,14 +61,14 @@ const TRACING_LEVEL: tracing::Level = tracing::Level::INFO;
// TODO: Should main returns 'Result'?
#[tokio::main]
async fn main() {
if process_args().await {
return;
}
tracing_subscriber::fmt()
.with_max_level(TRACING_LEVEL)
.init();
if process_args().await {
return;
}
event!(Level::INFO, "Starting Recipes as web server...");
let config = config::load();
@ -109,6 +109,26 @@ async fn main() {
"/recipe/set_group_comment",
put(services::ron::set_group_comment),
)
.route(
"/recipe/set_step_action",
put(services::ron::set_step_action),
)
.route(
"/recipe/set_ingredient_name",
put(services::ron::set_ingredient_name),
)
.route(
"/recipe/set_ingredient_comment",
put(services::ron::set_ingredient_comment),
)
.route(
"/recipe/set_ingredient_quantity",
put(services::ron::set_ingredient_quantity),
)
.route(
"/recipe/set_ingredient_unit",
put(services::ron::set_ingredient_unit),
)
.fallback(services::ron::not_found);
let fragments_routes = Router::new().route(

View file

@ -125,6 +125,44 @@ async fn check_user_rights_recipe_group(
}
}
async fn check_user_rights_recipe_step(
connection: &db::Connection,
user: &Option<model::User>,
step_id: i64,
) -> Result<()> {
if user.is_none()
|| !connection
.can_edit_recipe_step(user.as_ref().unwrap().id, step_id)
.await?
{
Err(ErrorResponse::from(ron_error(
StatusCode::UNAUTHORIZED,
"Action not authorized",
)))
} else {
Ok(())
}
}
async fn check_user_rights_recipe_ingredient(
connection: &db::Connection,
user: &Option<model::User>,
ingredient_id: i64,
) -> Result<()> {
if user.is_none()
|| !connection
.can_edit_recipe_ingredient(user.as_ref().unwrap().id, ingredient_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>,
@ -255,7 +293,6 @@ 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,
@ -318,6 +355,69 @@ pub async fn set_group_comment(
Ok(StatusCode::OK)
}
#[debug_handler]
pub async fn set_step_action(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
ExtractRon(ron): ExtractRon<common::ron_api::SetStepAction>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_step(&connection, &user, ron.step_id).await?;
connection.set_step_action(ron.step_id, &ron.action).await?;
Ok(StatusCode::OK)
}
#[debug_handler]
pub async fn set_ingredient_name(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
ExtractRon(ron): ExtractRon<common::ron_api::SetIngredientName>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?;
connection
.set_ingredient_name(ron.ingredient_id, &ron.name)
.await?;
Ok(StatusCode::OK)
}
#[debug_handler]
pub async fn set_ingredient_comment(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
ExtractRon(ron): ExtractRon<common::ron_api::SetIngredientComment>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?;
connection
.set_ingredient_comment(ron.ingredient_id, &ron.comment)
.await?;
Ok(StatusCode::OK)
}
#[debug_handler]
pub async fn set_ingredient_quantity(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
ExtractRon(ron): ExtractRon<common::ron_api::SetIngredientQuantity>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?;
connection
.set_ingredient_quantity(ron.ingredient_id, ron.quantity)
.await?;
Ok(StatusCode::OK)
}
#[debug_handler]
pub async fn set_ingredient_unit(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
ExtractRon(ron): ExtractRon<common::ron_api::SetIngredientUnit>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?;
connection
.set_ingredient_unit(ron.ingredient_id, &ron.unit)
.await?;
Ok(StatusCode::OK)
}
///// 404 /////
#[debug_handler]
pub async fn not_found(Extension(_user): Extension<Option<model::User>>) -> impl IntoResponse {

View file

@ -20,6 +20,6 @@
{% block body_container %}{% endblock %}
<footer class="footer-container">gburri - 2022</footer>
<footer class="footer-container">gburri - 2025</footer>
</body>
</html>

View file

@ -20,10 +20,11 @@
<textarea
id="text-area-description">{{ recipe.description }}</textarea>
<label for="input-estimated-time">Estimated time</label>
<label for="input-estimated-time">Estimated time [min]</label>
<input
id="input-estimated-time"
type="number"
step="1" min="0" max="1000"
value="
{% match recipe.estimated_time %}
{% when Some with (t) %}
@ -63,7 +64,7 @@
<div id="groups-container">
</div>
<input id="button-add-group" type="button" value="Add a group"/>
<input id="button-add-group" type="button" value="Add a group" />
<div id="hidden-templates">
<div class="group">
@ -73,15 +74,19 @@
<label for="input-group-comment">Comment</label>
<input class="input-group-comment" type="text" />
<input class="input-group-delete" type="button" value="Remove group" />
<div class="steps"></div>
<input class="button-add-step" type="button" value="Add a step"/>
<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>
<input class="input-step-delete" type="button" value="Remove step" />
<div class="ingredients"></div>
<input class="button-add-ingedient" type="button" value="Add an ingredient"/>
@ -89,13 +94,18 @@
<div class="ingredient">
<label for="input-ingredient-quantity">Quantity</label>
<input class="input-ingredient-quantity" type="number" />
<input class="input-ingredient-quantity" type="number" step="0.1" min="0" max="10000" />
<label for="input-ingredient-unit">Unity</label>
<label for="input-ingredient-unit">Unit</label>
<input class="input-ingredient-unit" type="text" />
<label for="input-ingredient-name">Name</label>
<input class="input-ingredient-name" type="text" />
<label for="input-ingredient-comment">Comment</label>
<input class="input-ingredient-comment" type="text" />
<input class="input-ingredient-delete" type="button" value="Remove ingredient" />
</div>
</div>
</div>