From: Greg Burri Date: Wed, 15 Jan 2025 23:17:08 +0000 (+0100) Subject: Ingredients can now be manually ordered X-Git-Url: https://git.euphorik.ch/?a=commitdiff_plain;h=ca2227037f608b37261a3491d04d57551e131f90;p=recipes.git Ingredients can now be manually ordered --- diff --git a/Cargo.lock b/Cargo.lock index 9905452..24c22c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -308,9 +308,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" dependencies = [ "serde", ] diff --git a/backend/scss/style.scss b/backend/scss/style.scss index ddd3d8c..8c9f9b0 100644 --- a/backend/scss/style.scss +++ b/backend/scss/style.scss @@ -138,10 +138,14 @@ body { .step { border: 0.1em solid lighten($color-3, 30%); + margin-top: 0px; + margin-bottom: 0px; } .ingredient { border: 0.1em solid lighten($color-3, 30%); + margin-top: 0px; + margin-bottom: 0px; } .dropzone { @@ -160,7 +164,6 @@ body { } } - #hidden-templates { display: none; } diff --git a/backend/sql/data_test.sql b/backend/sql/data_test.sql index 7e4a46b..c68174b 100644 --- a/backend/sql/data_test.sql +++ b/backend/sql/data_test.sql @@ -23,8 +23,8 @@ VALUES ( INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime]) VALUES (1, 1, 'Croissant au jambon', true, '2025-01-07T10:41:05.697884837+00:00'); -INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime]) -VALUES (2, 1, 'Gratin de thon aux olives', true, '2025-01-07T10:41:05.697884837+00:00'); +INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime], [servings], [estimated_time], [difficulty]) +VALUES (2, 1, 'Gratin de thon aux olives', true, '2025-01-07T10:41:05.697884837+00:00', 4, 40, 1); INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime]) VALUES (3, 1, 'Saumon en croute', true, '2025-01-07T10:41:05.697884837+00:00'); @@ -58,20 +58,20 @@ 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], [order], [step_id], [name], [comment], [quantity_value], [quantity_unit]) +VALUES (3, 0, 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], [order], [step_id], [name], [comment], [quantity_value], [quantity_unit]) +VALUES (4, 1, 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], [order], [step_id], [name], [comment], [quantity_value], [quantity_unit]) +VALUES (5, 2, 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], [order], [step_id], [name], [comment], [quantity_value], [quantity_unit]) +VALUES (6, 3, 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 [Ingredient] ([id], [order], [step_id], [name], [comment], [quantity_value], [quantity_unit]) +VALUES (7, 4, 3, "Olives farcies coupées en deuxs", "", 50, "g"); INSERT INTO [Group] ([id], [order], [recipe_id], [name], [comment]) diff --git a/backend/sql/version_1.sql b/backend/sql/version_1.sql index aa9c14d..be6ae95 100644 --- a/backend/sql/version_1.sql +++ b/backend/sql/version_1.sql @@ -56,6 +56,7 @@ CREATE TABLE [Recipe] ( [lang] TEXT NOT NULL DEFAULT 'en', [estimated_time] INTEGER, -- in [s]. [description] TEXT NOT NULL DEFAULT '', + -- 0: Unknown, 1: Easy, 2: Medium, 4: Hard. [difficulty] INTEGER NOT NULL DEFAULT 0, [servings] INTEGER DEFAULT 4, [is_published] INTEGER NOT NULL DEFAULT FALSE, @@ -64,6 +65,28 @@ CREATE TABLE [Recipe] ( FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE SET NULL ) STRICT; +CREATE TRIGGER [Recipe_trigger_update_difficulty] +BEFORE UPDATE OF [difficulty] +ON [Recipe] +BEGIN + SELECT + CASE + WHEN NEW.[difficulty] < 0 OR NEW.[difficulty] > 3 THEN + RAISE (ABORT, 'Invalid [difficulty] value') + END; +END; + +CREATE TRIGGER [Recipe_trigger_insert_difficulty] +BEFORE INSERT +ON [Recipe] +BEGIN + SELECT + CASE + WHEN NEW.[difficulty] < 0 OR NEW.[difficulty] > 3 THEN + RAISE (ABORT, 'Invalid [difficulty] value') + END; +END; + CREATE TABLE [Image] ( [Id] INTEGER PRIMARY KEY, [recipe_id] INTEGER NOT NULL, @@ -124,6 +147,7 @@ CREATE INDEX [Step_order_index] ON [Group]([order]); CREATE TABLE [Ingredient] ( [id] INTEGER PRIMARY KEY, + [order] INTEGER NOT NULL DEFAULT 0, [step_id] INTEGER NOT NULL, [name] TEXT NOT NULL DEFAULT '', @@ -134,14 +158,4 @@ CREATE TABLE [Ingredient] ( FOREIGN KEY([step_id]) REFERENCES [Step]([id]) ON DELETE CASCADE ) STRICT; --- CREATE TABLE [IntermediateSubstance] ( --- [id] INTEGER PRIMARY KEY, --- [name] TEXT NOT NULL DEFAULT '', --- [quantity_value] REAL, --- [quantity_unit] TEXT NOT NULL DEFAULT '', --- [output_group_id] INTEGER NOT NULL, --- [input_group_id] INTEGER NOT NULL, - --- FOREIGN KEY([output_group_id]) REFERENCES [group]([id]) ON DELETE CASCADE, --- FOREIGN KEY([input_group_id]) REFERENCES [group]([id]) ON DELETE CASCADE --- ) STRICT; +CREATE INDEX [Ingredient_order_index] ON [Ingredient]([order]); diff --git a/backend/src/data/db/recipe.rs b/backend/src/data/db/recipe.rs index f474220..64ed972 100644 --- a/backend/src/data/db/recipe.rs +++ b/backend/src/data/db/recipe.rs @@ -172,6 +172,33 @@ WHERE [Ingredient].[id] = $1 AND [user_id] = $2 .map_err(DBError::from) } + pub async fn can_edit_recipe_all_ingredients( + &self, + user_id: i64, + ingredients_ids: &[i64], + ) -> Result { + let params = (0..ingredients_ids.len()) + .map(|n| format!("${}", n + 2)) + .join(", "); + let query_str = format!( + 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] IN ({}) AND [user_id] = $1 + "#, + params + ); + + let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id); + for id in ingredients_ids { + query = query.bind(id); + } + Ok(query.fetch_one(&self.pool).await? == ingredients_ids.len() as u64) + } + pub async fn get_recipe(&self, id: i64, complete: bool) -> Result> { match sqlx::query_as::<_, model::Recipe>( r#" @@ -485,7 +512,7 @@ ORDER BY [order] SELECT [id], [name], [comment], [quantity_value], [quantity_unit] FROM [Ingredient] WHERE [step_id] = $1 -ORDER BY [name] +ORDER BY [order] "#, ) .bind(step.id) @@ -622,10 +649,27 @@ ORDER BY [name] } pub async fn add_recipe_ingredient(&self, step_id: i64) -> Result { - let db_result = sqlx::query("INSERT INTO [Ingredient] ([step_id]) VALUES ($1)") - .bind(step_id) - .execute(&self.pool) - .await?; + let mut tx = self.tx().await?; + + let last_order = sqlx::query_scalar( + "SELECT [order] FROM [Ingredient] WHERE [step_id] = $1 ORDER BY [order] DESC LIMIT 1", + ) + .bind(step_id) + .fetch_optional(&mut *tx) + .await? + .unwrap_or(-1); + + let db_result = sqlx::query( + r#" +INSERT INTO [Ingredient] ([step_id], [order]) +VALUES ($1, $2) + "#, + ) + .bind(step_id) + .bind(last_order as i64) + .execute(&mut *tx) + .await?; + Ok(db_result.last_insert_rowid()) } @@ -681,6 +725,22 @@ ORDER BY [name] .map(|_| ()) .map_err(DBError::from) } + + pub async fn set_ingredients_order(&self, ingredient_ids: &[i64]) -> Result<()> { + let mut tx = self.tx().await?; + + for (order, id) in ingredient_ids.iter().enumerate() { + sqlx::query("UPDATE [Ingredient] SET [order] = $2 WHERE [id] = $1") + .bind(id) + .bind(order as i64) + .execute(&mut *tx) + .await?; + } + + tx.commit().await?; + + Ok(()) + } } #[cfg(test)] diff --git a/backend/src/main.rs b/backend/src/main.rs index ad2fd2a..092c2b3 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -173,6 +173,10 @@ async fn main() { "/recipe/set_ingredient_unit", put(services::ron::set_ingredient_unit), ) + .route( + "/recipe/set_ingredients_order", + put(services::ron::set_ingredients_order), + ) .fallback(services::ron::not_found); let fragments_routes = Router::new().route( diff --git a/backend/src/services/ron.rs b/backend/src/services/ron.rs index 71e0635..ddf19b4 100644 --- a/backend/src/services/ron.rs +++ b/backend/src/services/ron.rs @@ -180,6 +180,25 @@ async fn check_user_rights_recipe_ingredient( } } +async fn check_user_rights_recipe_ingredients( + connection: &db::Connection, + user: &Option, + step_ids: &[i64], +) -> Result<()> { + if user.is_none() + || !connection + .can_edit_recipe_all_ingredients(user.as_ref().unwrap().id, step_ids) + .await? + { + Err(ErrorResponse::from(ron_error( + StatusCode::UNAUTHORIZED, + NOT_AUTHORIZED_MESSAGE, + ))) + } else { + Ok(()) + } +} + #[debug_handler] pub async fn set_recipe_title( State(connection): State, @@ -579,6 +598,19 @@ pub async fn set_ingredient_unit( Ok(StatusCode::OK) } +#[debug_handler] +pub async fn set_ingredients_order( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_ingredients(&connection, &user, &ron.ingredient_ids).await?; + connection + .set_ingredients_order(&ron.ingredient_ids) + .await?; + Ok(StatusCode::OK) +} + ///// 404 ///// #[debug_handler] pub async fn not_found(Extension(_user): Extension>) -> impl IntoResponse { diff --git a/backend/templates/recipe_edit.html b/backend/templates/recipe_edit.html index b830569..c14c8fc 100644 --- a/backend/templates/recipe_edit.html +++ b/backend/templates/recipe_edit.html @@ -116,6 +116,8 @@
+ + diff --git a/backend/templates/recipe_view.html b/backend/templates/recipe_view.html index 926aa91..39aea85 100644 --- a/backend/templates/recipe_view.html +++ b/backend/templates/recipe_view.html @@ -34,11 +34,48 @@ {% else %} {% endmatch %} + + {% match recipe.difficulty %} + {% when common::ron_api::Difficulty::Unknown %} + {% when common::ron_api::Difficulty::Easy %} + {{ tr.t(Sentence::RecipeDifficultyEasy) }} + {% when common::ron_api::Difficulty::Medium %} + {{ tr.t(Sentence::RecipeDifficultyMedium) }} + {% when common::ron_api::Difficulty::Hard %} + {{ tr.t(Sentence::RecipeDifficultyHard) }} + {% endmatch %} + + {% if !recipe.description.is_empty() %}
- {{ recipe.description.clone() }} + {{ recipe.description }}
{% endif %} + + {% for group in recipe.groups %} +
+

{{ group.name }}

+ +
+ {% for step in group.steps %} +
+ {% for ingredient in step.ingredients %} +
+ {% if let Some(quantity) = ingredient.quantity_value %} + {{ quantity +}} + {{+ ingredient.quantity_unit }} + {% endif +%} + {{+ ingredient.name }} +
+ {% endfor %} +
+
+ {{ step.action }} +
+ {% endfor %} +
+
+ {% endfor %}
{% endblock %} \ No newline at end of file diff --git a/common/src/ron_api.rs b/common/src/ron_api.rs index d533243..f3a6b1a 100644 --- a/common/src/ron_api.rs +++ b/common/src/ron_api.rs @@ -178,6 +178,11 @@ pub struct SetIngredientUnit { pub unit: String, } +#[derive(Serialize, Deserialize, Clone)] +pub struct SetIngredientOrders { + pub ingredient_ids: Vec, +} + #[derive(Serialize, Deserialize, Clone)] pub struct Tags { pub recipe_id: i64, diff --git a/frontend/src/recipe_edit.rs b/frontend/src/recipe_edit.rs index 5b8535c..e605028 100644 --- a/frontend/src/recipe_edit.rs +++ b/frontend/src/recipe_edit.rs @@ -574,6 +574,22 @@ fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingre ingredient_element.set_id(&format!("ingredient-{}", ingredient.id)); step_element.append_child(&ingredient_element).unwrap(); + set_draggable(&ingredient_element, "ingredient", |element| { + let element = element.clone(); + spawn_local(async move { + let ingredient_ids = element + .parent_element() + .unwrap() + .selector_all::(".ingredient") + .into_iter() + .map(|e| e.id()[11..].parse::().unwrap()) + .collect(); + + let body = ron_api::SetIngredientOrders { ingredient_ids }; + let _ = request::put::<(), _>("recipe/set_ingredients_order", body).await; + }); + }); + // Ingredient name. let name: HtmlInputElement = ingredient_element.selector(".input-ingredient-name"); name.set_value(&ingredient.name);