Shopping list items can now be checked/unchecked
authorGreg Burri <greg.burri@gmail.com>
Wed, 12 Feb 2025 22:13:48 +0000 (23:13 +0100)
committerGreg Burri <greg.burri@gmail.com>
Wed, 12 Feb 2025 22:13:48 +0000 (23:13 +0100)
14 files changed:
Cargo.lock
backend/src/data/db/recipe.rs
backend/src/data/db/shopping_list.rs
backend/src/main.rs
backend/src/services/ron.rs [deleted file]
backend/src/services/ron/calendar.rs [new file with mode: 0644]
backend/src/services/ron/mod.rs [new file with mode: 0644]
backend/src/services/ron/recipe.rs [new file with mode: 0644]
backend/src/services/ron/rights.rs [new file with mode: 0644]
backend/src/services/ron/shopping_list.rs [new file with mode: 0644]
backend/templates/home.html
common/src/ron_api.rs
frontend/src/home.rs
frontend/src/shopping_list.rs

index c99f661..7096d3a 100644 (file)
@@ -377,9 +377,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.28"
+version = "4.5.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff"
+checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -387,9 +387,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.27"
+version = "4.5.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
+checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9"
 dependencies = [
  "anstream",
  "anstyle",
@@ -1579,9 +1579,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 
 [[package]]
 name = "miniz_oxide"
-version = "0.8.3"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
+checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
 dependencies = [
  "adler2",
 ]
@@ -2138,9 +2138,9 @@ dependencies = [
 
 [[package]]
 name = "rustls"
-version = "0.23.22"
+version = "0.23.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7"
+checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
 dependencies = [
  "log",
  "once_cell",
@@ -2584,9 +2584,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
 
 [[package]]
 name = "stacker"
-version = "0.1.17"
+version = "0.1.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b"
+checksum = "1d08feb8f695b465baed819b03c128dc23f57a694510ab1f06c77f763975685e"
 dependencies = [
  "cc",
  "cfg-if",
index ee27553..00b5cec 100644 (file)
@@ -200,7 +200,7 @@ WHERE [Step].[id] IN ({}) AND ([user_id] = $1 OR (SELECT [is_admin] FROM [User]
     ) -> Result<bool> {
         sqlx::query_scalar(
             r#"
-SELECT COUNT(*)
+SELECT COUNT(*) = 1
 FROM [Recipe]
 INNER JOIN [User] ON [User].[id] = [Recipe].[user_id]
 INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
@@ -244,6 +244,21 @@ WHERE [Ingredient].[id] IN ({}) AND
         Ok(query.fetch_one(&self.pool).await? == ingredients_ids.len() as u64)
     }
 
+    pub async fn can_edit_shopping_list_entry(&self, user_id: i64, entry_id: i64) -> Result<bool> {
+        sqlx::query_scalar(
+            r#"
+SELECT COUNT(*) = 1
+FROM [ShoppingEntry]
+WHERE [id] = $1 AND ([user_id] = $2 OR (SELECT [is_admin] FROM [User] WHERE [id] = $2))
+            "#,
+        )
+        .bind(entry_id)
+        .bind(user_id)
+        .fetch_one(&self.pool)
+        .await
+        .map_err(DBError::from)
+    }
+
     pub async fn get_recipe(&self, id: i64, complete: bool) -> Result<Option<model::Recipe>> {
         match sqlx::query_as::<_, model::Recipe>(
             r#"
index ddb914e..1cd49c3 100644 (file)
@@ -27,7 +27,7 @@ FROM [ShoppingEntry]
        LEFT JOIN [RecipeScheduled] ON [RecipeScheduled].[id] = [ShoppingEntry].[recipe_scheduled_id]
        LEFT JOIN [Recipe] ON [Recipe].[id] = [RecipeScheduled].[recipe_id]
 WHERE [ShoppingEntry].[user_id] = $1
-ORDER BY [is_checked], [recipe_id], [name]
+ORDER BY [is_checked], [recipe_id], [name], [ShoppingEntry].[id]
         "#,
         )
         .bind(user_id)
@@ -35,4 +35,14 @@ ORDER BY [is_checked], [recipe_id], [name]
         .await
         .map_err(DBError::from)
     }
+
+    pub async fn set_entry_checked(&self, entry_id: i64, is_checked: bool) -> Result<()> {
+        sqlx::query("UPDATE [ShoppingEntry] SET [is_checked] = $2 WHERE [id] = $1")
+            .bind(entry_id)
+            .bind(is_checked)
+            .execute(&self.pool)
+            .await
+            .map(|_| ())
+            .map_err(DBError::from)
+    }
 }
index f6c9659..9e1b915 100644 (file)
@@ -107,92 +107,114 @@ async fn main() {
         // Disabled: update user profile is now made with a post data ('edit_user_post').
         // .route("/user/update", put(services::ron::update_user))
         .route("/set_lang", put(services::ron::set_lang))
-        .route("/recipe/get_titles", get(services::ron::get_titles))
-        .route("/recipe/set_title", put(services::ron::set_recipe_title))
+        .route("/recipe/get_titles", get(services::ron::recipe::get_titles))
+        .route("/recipe/set_title", put(services::ron::recipe::set_title))
         .route(
             "/recipe/set_description",
-            put(services::ron::set_recipe_description),
+            put(services::ron::recipe::set_description),
+        )
+        .route(
+            "/recipe/set_servings",
+            put(services::ron::recipe::set_servings),
         )
-        .route("/recipe/set_servings", put(services::ron::set_servings))
         .route(
             "/recipe/set_estimated_time",
-            put(services::ron::set_estimated_time),
+            put(services::ron::recipe::set_estimated_time),
+        )
+        .route("/recipe/get_tags", get(services::ron::recipe::get_tags))
+        .route("/recipe/add_tags", post(services::ron::recipe::add_tags))
+        .route("/recipe/rm_tags", delete(services::ron::recipe::rm_tags))
+        .route(
+            "/recipe/set_difficulty",
+            put(services::ron::recipe::set_difficulty),
+        )
+        .route(
+            "/recipe/set_language",
+            put(services::ron::recipe::set_language),
         )
-        .route("/recipe/get_tags", get(services::ron::get_tags))
-        .route("/recipe/add_tags", post(services::ron::add_tags))
-        .route("/recipe/rm_tags", delete(services::ron::rm_tags))
-        .route("/recipe/set_difficulty", put(services::ron::set_difficulty))
-        .route("/recipe/set_language", put(services::ron::set_language))
         .route(
             "/recipe/set_is_published",
-            put(services::ron::set_is_published),
+            put(services::ron::recipe::set_is_published),
+        )
+        .route("/recipe/remove", delete(services::ron::recipe::rm))
+        .route("/recipe/get_groups", get(services::ron::recipe::get_groups))
+        .route("/recipe/add_group", post(services::ron::recipe::add_group))
+        .route(
+            "/recipe/remove_group",
+            delete(services::ron::recipe::rm_group),
+        )
+        .route(
+            "/recipe/set_group_name",
+            put(services::ron::recipe::set_group_name),
         )
-        .route("/recipe/remove", delete(services::ron::rm))
-        .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),
+            put(services::ron::recipe::set_group_comment),
         )
         .route(
             "/recipe/set_groups_order",
-            put(services::ron::set_groups_order),
+            put(services::ron::recipe::set_groups_order),
+        )
+        .route("/recipe/add_step", post(services::ron::recipe::add_step))
+        .route(
+            "/recipe/remove_step",
+            delete(services::ron::recipe::rm_step),
         )
-        .route("/recipe/add_step", post(services::ron::add_step))
-        .route("/recipe/remove_step", delete(services::ron::rm_step))
         .route(
             "/recipe/set_step_action",
-            put(services::ron::set_step_action),
+            put(services::ron::recipe::set_step_action),
         )
         .route(
             "/recipe/set_steps_order",
-            put(services::ron::set_steps_order),
+            put(services::ron::recipe::set_steps_order),
         )
         .route(
             "/recipe/add_ingredient",
-            post(services::ron::add_ingredient),
+            post(services::ron::recipe::add_ingredient),
         )
         .route(
             "/recipe/remove_ingredient",
-            delete(services::ron::rm_ingredient),
+            delete(services::ron::recipe::rm_ingredient),
         )
         .route(
             "/recipe/set_ingredient_name",
-            put(services::ron::set_ingredient_name),
+            put(services::ron::recipe::set_ingredient_name),
         )
         .route(
             "/recipe/set_ingredient_comment",
-            put(services::ron::set_ingredient_comment),
+            put(services::ron::recipe::set_ingredient_comment),
         )
         .route(
             "/recipe/set_ingredient_quantity",
-            put(services::ron::set_ingredient_quantity),
+            put(services::ron::recipe::set_ingredient_quantity),
         )
         .route(
             "/recipe/set_ingredient_unit",
-            put(services::ron::set_ingredient_unit),
+            put(services::ron::recipe::set_ingredient_unit),
         )
         .route(
             "/recipe/set_ingredients_order",
-            put(services::ron::set_ingredients_order),
+            put(services::ron::recipe::set_ingredients_order),
         )
         .route(
             "/calendar/get_scheduled_recipes",
-            get(services::ron::get_scheduled_recipes),
+            get(services::ron::calendar::get_scheduled_recipes),
         )
         .route(
             "/calendar/schedule_recipe",
-            post(services::ron::schedule_recipe),
+            post(services::ron::calendar::schedule_recipe),
         )
         .route(
             "/calendar/remove_scheduled_recipe",
-            delete(services::ron::rm_scheduled_recipe),
+            delete(services::ron::calendar::rm_scheduled_recipe),
         )
         .route(
             "/shopping_list/get_list",
-            get(services::ron::get_shopping_list),
+            get(services::ron::shopping_list::get),
+        )
+        .route(
+            "/shopping_list/set_checked",
+            put(services::ron::shopping_list::set_entry_checked),
         )
         .fallback(services::ron::not_found);
 
diff --git a/backend/src/services/ron.rs b/backend/src/services/ron.rs
deleted file mode 100644 (file)
index 7090ddb..0000000
+++ /dev/null
@@ -1,744 +0,0 @@
-use axum::{
-    debug_handler,
-    extract::{Extension, State},
-    http::{HeaderMap, StatusCode},
-    response::{ErrorResponse, IntoResponse, Response, Result},
-};
-use axum_extra::extract::{
-    cookie::{Cookie, CookieJar},
-    Query,
-};
-use serde::{Deserialize, Serialize};
-// use tracing::{event, Level};
-
-use crate::{
-    consts,
-    data::{self, db},
-    model,
-    ron_extractor::ExtractRon,
-    ron_utils::{ron_error, ron_response_ok},
-};
-
-const NOT_AUTHORIZED_MESSAGE: &str = "Action not authorized";
-
-#[derive(Deserialize)]
-pub struct Id {
-    id: i64,
-}
-
-#[derive(Deserialize, Serialize)]
-pub struct Ids {
-    ids: Vec<i64>,
-}
-
-// #[allow(dead_code)]
-// #[debug_handler]
-// pub async fn update_user(
-//     State(connection): State<db::Connection>,
-//     Extension(user): Extension<Option<model::User>>,
-//     ExtractRon(ron): ExtractRon<common::ron_api::UpdateProfile>,
-// ) -> Result<StatusCode> {
-//     if let Some(user) = user {
-//         connection
-//             .update_user(
-//                 user.id,
-//                 ron.email.as_deref().map(str::trim),
-//                 ron.name.as_deref(),
-//                 ron.password.as_deref(),
-//             )
-//             .await?;
-//     } else {
-//         return Err(ErrorResponse::from(ron_error(
-//             StatusCode::UNAUTHORIZED,
-//             NOT_AUTHORIZED_MESSAGE,
-//         )));
-//     }
-//     Ok(StatusCode::OK)
-// }
-
-#[debug_handler]
-pub async fn set_lang(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    headers: HeaderMap,
-    ExtractRon(ron): ExtractRon<common::ron_api::SetLang>,
-) -> Result<(CookieJar, StatusCode)> {
-    let mut jar = CookieJar::from_headers(&headers);
-    if let Some(user) = user {
-        connection.set_user_lang(user.id, &ron.lang).await?;
-    } else {
-        let cookie = Cookie::build((consts::COOKIE_LANG_NAME, ron.lang)).path("/");
-        jar = jar.add(cookie);
-    }
-    Ok((jar, StatusCode::OK))
-}
-
-/*** Rights ***/
-
-async fn check_user_rights_recipe(
-    connection: &db::Connection,
-    user: &Option<model::User>,
-    recipe_id: i64,
-) -> Result<()> {
-    if user.is_none()
-        || !connection
-            .can_edit_recipe(user.as_ref().unwrap().id, recipe_id)
-            .await?
-    {
-        Err(ErrorResponse::from(ron_error(
-            StatusCode::UNAUTHORIZED,
-            NOT_AUTHORIZED_MESSAGE,
-        )))
-    } else {
-        Ok(())
-    }
-}
-
-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,
-            NOT_AUTHORIZED_MESSAGE,
-        )))
-    } else {
-        Ok(())
-    }
-}
-
-async fn check_user_rights_recipe_groups(
-    connection: &db::Connection,
-    user: &Option<model::User>,
-    group_ids: &[i64],
-) -> Result<()> {
-    if user.is_none()
-        || !connection
-            .can_edit_recipe_all_groups(user.as_ref().unwrap().id, group_ids)
-            .await?
-    {
-        Err(ErrorResponse::from(ron_error(
-            StatusCode::UNAUTHORIZED,
-            NOT_AUTHORIZED_MESSAGE,
-        )))
-    } else {
-        Ok(())
-    }
-}
-
-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,
-            NOT_AUTHORIZED_MESSAGE,
-        )))
-    } else {
-        Ok(())
-    }
-}
-
-async fn check_user_rights_recipe_steps(
-    connection: &db::Connection,
-    user: &Option<model::User>,
-    step_ids: &[i64],
-) -> Result<()> {
-    if user.is_none()
-        || !connection
-            .can_edit_recipe_all_steps(user.as_ref().unwrap().id, step_ids)
-            .await?
-    {
-        Err(ErrorResponse::from(ron_error(
-            StatusCode::UNAUTHORIZED,
-            NOT_AUTHORIZED_MESSAGE,
-        )))
-    } 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,
-            NOT_AUTHORIZED_MESSAGE,
-        )))
-    } else {
-        Ok(())
-    }
-}
-
-async fn check_user_rights_recipe_ingredients(
-    connection: &db::Connection,
-    user: &Option<model::User>,
-    ingredient_ids: &[i64],
-) -> Result<()> {
-    if user.is_none()
-        || !connection
-            .can_edit_recipe_all_ingredients(user.as_ref().unwrap().id, ingredient_ids)
-            .await?
-    {
-        Err(ErrorResponse::from(ron_error(
-            StatusCode::UNAUTHORIZED,
-            NOT_AUTHORIZED_MESSAGE,
-        )))
-    } else {
-        Ok(())
-    }
-}
-
-/*** Recipe ***/
-
-/// Ask recipe titles associated with each given id. The returned titles are in the same order
-/// as the given ids.
-#[debug_handler]
-pub async fn get_titles(
-    State(connection): State<db::Connection>,
-    recipe_ids: Query<Ids>,
-) -> Result<impl IntoResponse> {
-    Ok(ron_response_ok(common::ron_api::Strings {
-        strs: connection.get_recipe_titles(&recipe_ids.ids).await?,
-    }))
-}
-
-#[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_recipe(&connection, &user, ron.recipe_id).await?;
-    connection
-        .set_recipe_title(ron.recipe_id, &ron.title)
-        .await?;
-    Ok(StatusCode::OK)
-}
-
-#[debug_handler]
-pub async fn set_recipe_description(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeDescription>,
-) -> Result<StatusCode> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
-    connection
-        .set_recipe_description(ron.recipe_id, &ron.description)
-        .await?;
-    Ok(StatusCode::OK)
-}
-
-#[debug_handler]
-pub async fn set_servings(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeServings>,
-) -> Result<StatusCode> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
-    connection
-        .set_recipe_servings(ron.recipe_id, ron.servings)
-        .await?;
-    Ok(StatusCode::OK)
-}
-
-#[debug_handler]
-pub async fn set_estimated_time(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeEstimatedTime>,
-) -> Result<StatusCode> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
-    connection
-        .set_recipe_estimated_time(ron.recipe_id, ron.estimated_time)
-        .await?;
-    Ok(StatusCode::OK)
-}
-
-#[debug_handler]
-pub async fn get_tags(
-    State(connection): State<db::Connection>,
-    recipe_id: Query<Id>,
-) -> Result<impl IntoResponse> {
-    Ok(ron_response_ok(common::ron_api::Tags {
-        recipe_id: recipe_id.id,
-        tags: connection.get_recipes_tags(recipe_id.id).await?,
-    }))
-}
-
-#[debug_handler]
-pub async fn add_tags(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::Tags>,
-) -> Result<impl IntoResponse> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
-    connection
-        .add_recipe_tags(
-            ron.recipe_id,
-            &ron.tags
-                .into_iter()
-                .map(|tag| tag.to_lowercase())
-                .collect::<Vec<_>>(),
-        )
-        .await?;
-    Ok(StatusCode::OK)
-}
-
-#[debug_handler]
-pub async fn rm_tags(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::Tags>,
-) -> Result<impl IntoResponse> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
-    connection.rm_recipe_tags(ron.recipe_id, &ron.tags).await?;
-    Ok(StatusCode::OK)
-}
-
-#[debug_handler]
-pub async fn set_difficulty(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeDifficulty>,
-) -> Result<StatusCode> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
-    connection
-        .set_recipe_difficulty(ron.recipe_id, ron.difficulty)
-        .await?;
-    Ok(StatusCode::OK)
-}
-
-#[debug_handler]
-pub async fn set_language(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeLanguage>,
-) -> Result<StatusCode> {
-    if !crate::translation::available_codes()
-        .iter()
-        .any(|&l| l == ron.lang)
-    {
-        // TODO: log?
-        return Ok(StatusCode::BAD_REQUEST);
-    }
-
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
-    connection
-        .set_recipe_language(ron.recipe_id, &ron.lang)
-        .await?;
-    Ok(StatusCode::OK)
-}
-
-#[debug_handler]
-pub async fn set_is_published(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::SetIsPublished>,
-) -> Result<StatusCode> {
-    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)
-}
-
-#[debug_handler]
-pub async fn rm(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::Id>,
-) -> Result<impl IntoResponse> {
-    check_user_rights_recipe(&connection, &user, ron.id).await?;
-    connection.rm_recipe(ron.id).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,
-        }
-    }
-}
-
-#[debug_handler]
-pub async fn get_groups(
-    State(connection): State<db::Connection>,
-    recipe_id: Query<Id>,
-) -> Result<impl IntoResponse> {
-    // Here we don't check user rights on purpose.
-    Ok(ron_response_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::Id>,
-) -> Result<impl IntoResponse> {
-    check_user_rights_recipe(&connection, &user, ron.id).await?;
-    let id = connection.add_recipe_group(ron.id).await?;
-
-    Ok(ron_response_ok(common::ron_api::Id { 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::Id>,
-) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_group(&connection, &user, ron.id).await?;
-    connection.rm_recipe_group(ron.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)
-}
-
-#[debug_handler]
-pub async fn set_groups_order(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::Ids>,
-) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_groups(&connection, &user, &ron.ids).await?;
-    connection.set_groups_order(&ron.ids).await?;
-    Ok(StatusCode::OK)
-}
-
-#[debug_handler]
-pub async fn add_step(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::Id>,
-) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_group(&connection, &user, ron.id).await?;
-    let id = connection.add_recipe_step(ron.id).await?;
-
-    Ok(ron_response_ok(common::ron_api::Id { id }))
-}
-
-#[debug_handler]
-pub async fn rm_step(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::Id>,
-) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_step(&connection, &user, ron.id).await?;
-    connection.rm_recipe_step(ron.id).await?;
-    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_steps_order(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::Ids>,
-) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_steps(&connection, &user, &ron.ids).await?;
-    connection.set_steps_order(&ron.ids).await?;
-    Ok(StatusCode::OK)
-}
-
-#[debug_handler]
-pub async fn add_ingredient(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::Id>,
-) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_step(&connection, &user, ron.id).await?;
-    let id = connection.add_recipe_ingredient(ron.id).await?;
-    Ok(ron_response_ok(common::ron_api::Id { id }))
-}
-
-#[debug_handler]
-pub async fn rm_ingredient(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::Id>,
-) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_ingredient(&connection, &user, ron.id).await?;
-    connection.rm_recipe_ingredient(ron.id).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)
-}
-
-#[debug_handler]
-pub async fn set_ingredients_order(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::Ids>,
-) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_ingredients(&connection, &user, &ron.ids).await?;
-    connection.set_ingredients_order(&ron.ids).await?;
-    Ok(StatusCode::OK)
-}
-
-/*** Calendar ***/
-
-#[debug_handler]
-pub async fn get_scheduled_recipes(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    date_range: Query<common::ron_api::DateRange>,
-) -> Result<impl IntoResponse> {
-    if let Some(user) = user {
-        Ok(ron_response_ok(common::ron_api::ScheduledRecipes {
-            recipes: connection
-                .get_scheduled_recipes(user.id, date_range.start_date, date_range.end_date)
-                .await?,
-        }))
-    } else {
-        Err(ErrorResponse::from(ron_error(
-            StatusCode::UNAUTHORIZED,
-            NOT_AUTHORIZED_MESSAGE,
-        )))
-    }
-}
-
-impl From<data::db::recipe::AddScheduledRecipeResult> for common::ron_api::ScheduleRecipeResult {
-    fn from(db_res: data::db::recipe::AddScheduledRecipeResult) -> Self {
-        match db_res {
-            db::recipe::AddScheduledRecipeResult::Ok => Self::Ok,
-            db::recipe::AddScheduledRecipeResult::RecipeAlreadyScheduledAtThisDate => {
-                Self::RecipeAlreadyScheduledAtThisDate
-            }
-        }
-    }
-}
-
-#[debug_handler]
-pub async fn schedule_recipe(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::ScheduleRecipe>,
-) -> Result<Response> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
-    if let Some(user) = user {
-        connection
-            .add_scheduled_recipe(
-                user.id,
-                ron.recipe_id,
-                ron.date,
-                ron.servings,
-                ron.add_ingredients_to_shopping_list,
-            )
-            .await
-            .map(|res| {
-                ron_response_ok(common::ron_api::ScheduleRecipeResult::from(res)).into_response()
-            })
-            .map_err(ErrorResponse::from)
-    } else {
-        Ok(StatusCode::OK.into_response())
-    }
-}
-
-#[debug_handler]
-pub async fn rm_scheduled_recipe(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    ExtractRon(ron): ExtractRon<common::ron_api::RemoveScheduledRecipe>,
-) -> Result<impl IntoResponse> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
-    if let Some(user) = user {
-        connection
-            .rm_scheduled_recipe(
-                user.id,
-                ron.recipe_id,
-                ron.date,
-                ron.remove_ingredients_from_shopping_list,
-            )
-            .await?;
-    }
-    Ok(StatusCode::OK)
-}
-
-/*** Shopping list ***/
-
-impl From<model::ShoppingListItem> for common::ron_api::ShoppingListItem {
-    fn from(item: model::ShoppingListItem) -> Self {
-        Self {
-            id: item.id,
-            name: item.name,
-            quantity_value: item.quantity_value,
-            quantity_unit: item.quantity_unit,
-            recipe_id: item.recipe_id,
-            recipe_title: item.recipe_title,
-            date: item.date,
-            is_checked: item.is_checked,
-        }
-    }
-}
-
-#[debug_handler]
-pub async fn get_shopping_list(
-    State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-) -> Result<impl IntoResponse> {
-    if let Some(user) = user {
-        Ok(ron_response_ok(
-            connection
-                .get_shopping_list(user.id)
-                .await?
-                .into_iter()
-                .map(common::ron_api::ShoppingListItem::from)
-                .collect::<Vec<_>>(),
-        ))
-    } else {
-        Err(ErrorResponse::from(ron_error(
-            StatusCode::UNAUTHORIZED,
-            NOT_AUTHORIZED_MESSAGE,
-        )))
-    }
-}
-
-/*** 404 ***/
-
-#[debug_handler]
-pub async fn not_found(Extension(_user): Extension<Option<model::User>>) -> impl IntoResponse {
-    ron_error(StatusCode::NOT_FOUND, "Not found")
-}
diff --git a/backend/src/services/ron/calendar.rs b/backend/src/services/ron/calendar.rs
new file mode 100644 (file)
index 0000000..2f3a6a2
--- /dev/null
@@ -0,0 +1,94 @@
+use axum::{
+    debug_handler,
+    extract::{Extension, State},
+    http::StatusCode,
+    response::{ErrorResponse, IntoResponse, Response, Result},
+};
+use axum_extra::extract::Query;
+// use tracing::{event, Level};
+
+use crate::{
+    data::{self, db},
+    model,
+    ron_extractor::ExtractRon,
+    ron_utils::{ron_error, ron_response_ok},
+};
+
+use super::rights::*;
+
+#[debug_handler]
+pub async fn get_scheduled_recipes(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    date_range: Query<common::ron_api::DateRange>,
+) -> Result<impl IntoResponse> {
+    if let Some(user) = user {
+        Ok(ron_response_ok(common::ron_api::ScheduledRecipes {
+            recipes: connection
+                .get_scheduled_recipes(user.id, date_range.start_date, date_range.end_date)
+                .await?,
+        }))
+    } else {
+        Err(ErrorResponse::from(ron_error(
+            StatusCode::UNAUTHORIZED,
+            super::NOT_AUTHORIZED_MESSAGE,
+        )))
+    }
+}
+
+impl From<data::db::recipe::AddScheduledRecipeResult> for common::ron_api::ScheduleRecipeResult {
+    fn from(db_res: data::db::recipe::AddScheduledRecipeResult) -> Self {
+        match db_res {
+            db::recipe::AddScheduledRecipeResult::Ok => Self::Ok,
+            db::recipe::AddScheduledRecipeResult::RecipeAlreadyScheduledAtThisDate => {
+                Self::RecipeAlreadyScheduledAtThisDate
+            }
+        }
+    }
+}
+
+#[debug_handler]
+pub async fn schedule_recipe(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<common::ron_api::ScheduleRecipe>,
+) -> Result<Response> {
+    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    if let Some(user) = user {
+        connection
+            .add_scheduled_recipe(
+                user.id,
+                ron.recipe_id,
+                ron.date,
+                ron.servings,
+                ron.add_ingredients_to_shopping_list,
+            )
+            .await
+            .map(|res| {
+                ron_response_ok(common::ron_api::ScheduleRecipeResult::from(res)).into_response()
+            })
+            .map_err(ErrorResponse::from)
+    } else {
+        Ok(StatusCode::OK.into_response())
+    }
+}
+
+#[debug_handler]
+pub async fn rm_scheduled_recipe(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<common::ron_api::RemoveScheduledRecipe>,
+) -> Result<impl IntoResponse> {
+    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    if let Some(user) = user {
+        connection
+            .rm_scheduled_recipe(
+                user.id,
+                ron.recipe_id,
+                ron.date,
+                ron.remove_ingredients_from_shopping_list,
+            )
+            .await?;
+    }
+    Ok(StatusCode::OK)
+}
diff --git a/backend/src/services/ron/mod.rs b/backend/src/services/ron/mod.rs
new file mode 100644 (file)
index 0000000..1092f76
--- /dev/null
@@ -0,0 +1,41 @@
+use axum::{
+    debug_handler,
+    extract::{Extension, State},
+    http::{HeaderMap, StatusCode},
+    response::{IntoResponse, Result},
+};
+use axum_extra::extract::cookie::{Cookie, CookieJar};
+// use tracing::{event, Level};
+
+use crate::{consts, data::db, model, ron_extractor::ExtractRon, ron_utils::ron_error};
+
+pub mod calendar;
+pub mod recipe;
+mod rights;
+pub mod shopping_list;
+
+const NOT_AUTHORIZED_MESSAGE: &str = "Action not authorized";
+
+#[debug_handler]
+pub async fn set_lang(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    headers: HeaderMap,
+    ExtractRon(ron): ExtractRon<common::ron_api::SetLang>,
+) -> Result<(CookieJar, StatusCode)> {
+    let mut jar = CookieJar::from_headers(&headers);
+    if let Some(user) = user {
+        connection.set_user_lang(user.id, &ron.lang).await?;
+    } else {
+        let cookie = Cookie::build((consts::COOKIE_LANG_NAME, ron.lang)).path("/");
+        jar = jar.add(cookie);
+    }
+    Ok((jar, StatusCode::OK))
+}
+
+/*** 404 ***/
+
+#[debug_handler]
+pub async fn not_found(Extension(_user): Extension<Option<model::User>>) -> impl IntoResponse {
+    ron_error(StatusCode::NOT_FOUND, "Not found")
+}
diff --git a/backend/src/services/ron/recipe.rs b/backend/src/services/ron/recipe.rs
new file mode 100644 (file)
index 0000000..9cd7db4
--- /dev/null
@@ -0,0 +1,417 @@
+use axum::{
+    debug_handler,
+    extract::{Extension, State},
+    http::StatusCode,
+    response::{IntoResponse, Result},
+};
+use axum_extra::extract::Query;
+use common::ron_api;
+// use tracing::{event, Level};
+
+use crate::{data::db, model, ron_extractor::ExtractRon, ron_utils::ron_response_ok};
+
+use super::rights::*;
+
+/// Ask recipe titles associated with each given id. The returned titles are in the same order
+/// as the given ids.
+#[debug_handler]
+pub async fn get_titles(
+    State(connection): State<db::Connection>,
+    recipe_ids: Query<ron_api::Ids>,
+) -> Result<impl IntoResponse> {
+    Ok(ron_response_ok(ron_api::Strings {
+        strs: connection.get_recipe_titles(&recipe_ids.ids).await?,
+    }))
+}
+
+#[debug_handler]
+pub async fn set_title(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::SetRecipeTitle>,
+) -> Result<StatusCode> {
+    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    connection
+        .set_recipe_title(ron.recipe_id, &ron.title)
+        .await?;
+    Ok(StatusCode::OK)
+}
+
+#[debug_handler]
+pub async fn set_description(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::SetRecipeDescription>,
+) -> Result<StatusCode> {
+    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    connection
+        .set_recipe_description(ron.recipe_id, &ron.description)
+        .await?;
+    Ok(StatusCode::OK)
+}
+
+#[debug_handler]
+pub async fn set_servings(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::SetRecipeServings>,
+) -> Result<StatusCode> {
+    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    connection
+        .set_recipe_servings(ron.recipe_id, ron.servings)
+        .await?;
+    Ok(StatusCode::OK)
+}
+
+#[debug_handler]
+pub async fn set_estimated_time(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::SetRecipeEstimatedTime>,
+) -> Result<StatusCode> {
+    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    connection
+        .set_recipe_estimated_time(ron.recipe_id, ron.estimated_time)
+        .await?;
+    Ok(StatusCode::OK)
+}
+
+#[debug_handler]
+pub async fn get_tags(
+    State(connection): State<db::Connection>,
+    recipe_id: Query<ron_api::Id>,
+) -> Result<impl IntoResponse> {
+    Ok(ron_response_ok(ron_api::Tags {
+        recipe_id: recipe_id.id,
+        tags: connection.get_recipes_tags(recipe_id.id).await?,
+    }))
+}
+
+#[debug_handler]
+pub async fn add_tags(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::Tags>,
+) -> Result<impl IntoResponse> {
+    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    connection
+        .add_recipe_tags(
+            ron.recipe_id,
+            &ron.tags
+                .into_iter()
+                .map(|tag| tag.to_lowercase())
+                .collect::<Vec<_>>(),
+        )
+        .await?;
+    Ok(StatusCode::OK)
+}
+
+#[debug_handler]
+pub async fn rm_tags(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::Tags>,
+) -> Result<impl IntoResponse> {
+    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    connection.rm_recipe_tags(ron.recipe_id, &ron.tags).await?;
+    Ok(StatusCode::OK)
+}
+
+#[debug_handler]
+pub async fn set_difficulty(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::SetRecipeDifficulty>,
+) -> Result<StatusCode> {
+    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    connection
+        .set_recipe_difficulty(ron.recipe_id, ron.difficulty)
+        .await?;
+    Ok(StatusCode::OK)
+}
+
+#[debug_handler]
+pub async fn set_language(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::SetRecipeLanguage>,
+) -> Result<StatusCode> {
+    if !crate::translation::available_codes()
+        .iter()
+        .any(|&l| l == ron.lang)
+    {
+        // TODO: log?
+        return Ok(StatusCode::BAD_REQUEST);
+    }
+
+    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    connection
+        .set_recipe_language(ron.recipe_id, &ron.lang)
+        .await?;
+    Ok(StatusCode::OK)
+}
+
+#[debug_handler]
+pub async fn set_is_published(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::SetIsPublished>,
+) -> Result<StatusCode> {
+    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)
+}
+
+#[debug_handler]
+pub async fn rm(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::Id>,
+) -> Result<impl IntoResponse> {
+    check_user_rights_recipe(&connection, &user, ron.id).await?;
+    connection.rm_recipe(ron.id).await?;
+    Ok(StatusCode::OK)
+}
+
+impl From<model::Group> for 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(ron_api::Step::from).collect(),
+        }
+    }
+}
+
+impl From<model::Step> for ron_api::Step {
+    fn from(step: model::Step) -> Self {
+        Self {
+            id: step.id,
+            action: step.action,
+            ingredients: step
+                .ingredients
+                .into_iter()
+                .map(ron_api::Ingredient::from)
+                .collect(),
+        }
+    }
+}
+
+impl From<model::Ingredient> for 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,
+        }
+    }
+}
+
+#[debug_handler]
+pub async fn get_groups(
+    State(connection): State<db::Connection>,
+    recipe_id: Query<ron_api::Id>,
+) -> Result<impl IntoResponse> {
+    // Here we don't check user rights on purpose.
+    Ok(ron_response_ok(
+        connection
+            .get_groups(recipe_id.id)
+            .await?
+            .into_iter()
+            .map(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<ron_api::Id>,
+) -> Result<impl IntoResponse> {
+    check_user_rights_recipe(&connection, &user, ron.id).await?;
+    let id = connection.add_recipe_group(ron.id).await?;
+
+    Ok(ron_response_ok(ron_api::Id { id }))
+}
+
+#[debug_handler]
+pub async fn rm_group(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::Id>,
+) -> Result<impl IntoResponse> {
+    check_user_rights_recipe_group(&connection, &user, ron.id).await?;
+    connection.rm_recipe_group(ron.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<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<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)
+}
+
+#[debug_handler]
+pub async fn set_groups_order(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::Ids>,
+) -> Result<impl IntoResponse> {
+    check_user_rights_recipe_groups(&connection, &user, &ron.ids).await?;
+    connection.set_groups_order(&ron.ids).await?;
+    Ok(StatusCode::OK)
+}
+
+#[debug_handler]
+pub async fn add_step(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::Id>,
+) -> Result<impl IntoResponse> {
+    check_user_rights_recipe_group(&connection, &user, ron.id).await?;
+    let id = connection.add_recipe_step(ron.id).await?;
+
+    Ok(ron_response_ok(ron_api::Id { id }))
+}
+
+#[debug_handler]
+pub async fn rm_step(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::Id>,
+) -> Result<impl IntoResponse> {
+    check_user_rights_recipe_step(&connection, &user, ron.id).await?;
+    connection.rm_recipe_step(ron.id).await?;
+    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<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_steps_order(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::Ids>,
+) -> Result<impl IntoResponse> {
+    check_user_rights_recipe_steps(&connection, &user, &ron.ids).await?;
+    connection.set_steps_order(&ron.ids).await?;
+    Ok(StatusCode::OK)
+}
+
+#[debug_handler]
+pub async fn add_ingredient(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::Id>,
+) -> Result<impl IntoResponse> {
+    check_user_rights_recipe_step(&connection, &user, ron.id).await?;
+    let id = connection.add_recipe_ingredient(ron.id).await?;
+    Ok(ron_response_ok(ron_api::Id { id }))
+}
+
+#[debug_handler]
+pub async fn rm_ingredient(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::Id>,
+) -> Result<impl IntoResponse> {
+    check_user_rights_recipe_ingredient(&connection, &user, ron.id).await?;
+    connection.rm_recipe_ingredient(ron.id).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<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<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<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<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)
+}
+
+#[debug_handler]
+pub async fn set_ingredients_order(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::Ids>,
+) -> Result<impl IntoResponse> {
+    check_user_rights_recipe_ingredients(&connection, &user, &ron.ids).await?;
+    connection.set_ingredients_order(&ron.ids).await?;
+    Ok(StatusCode::OK)
+}
diff --git a/backend/src/services/ron/rights.rs b/backend/src/services/ron/rights.rs
new file mode 100644 (file)
index 0000000..2554c80
--- /dev/null
@@ -0,0 +1,158 @@
+use axum::{
+    http::StatusCode,
+    response::{ErrorResponse, Result},
+};
+
+use crate::{data::db, model, ron_utils::ron_error};
+
+pub async fn check_user_rights_recipe(
+    connection: &db::Connection,
+    user: &Option<model::User>,
+    recipe_id: i64,
+) -> Result<()> {
+    if user.is_none()
+        || !connection
+            .can_edit_recipe(user.as_ref().unwrap().id, recipe_id)
+            .await?
+    {
+        Err(ErrorResponse::from(ron_error(
+            StatusCode::UNAUTHORIZED,
+            super::NOT_AUTHORIZED_MESSAGE,
+        )))
+    } else {
+        Ok(())
+    }
+}
+
+pub 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,
+            super::NOT_AUTHORIZED_MESSAGE,
+        )))
+    } else {
+        Ok(())
+    }
+}
+
+pub async fn check_user_rights_recipe_groups(
+    connection: &db::Connection,
+    user: &Option<model::User>,
+    group_ids: &[i64],
+) -> Result<()> {
+    if user.is_none()
+        || !connection
+            .can_edit_recipe_all_groups(user.as_ref().unwrap().id, group_ids)
+            .await?
+    {
+        Err(ErrorResponse::from(ron_error(
+            StatusCode::UNAUTHORIZED,
+            super::NOT_AUTHORIZED_MESSAGE,
+        )))
+    } else {
+        Ok(())
+    }
+}
+
+pub 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,
+            super::NOT_AUTHORIZED_MESSAGE,
+        )))
+    } else {
+        Ok(())
+    }
+}
+
+pub async fn check_user_rights_recipe_steps(
+    connection: &db::Connection,
+    user: &Option<model::User>,
+    step_ids: &[i64],
+) -> Result<()> {
+    if user.is_none()
+        || !connection
+            .can_edit_recipe_all_steps(user.as_ref().unwrap().id, step_ids)
+            .await?
+    {
+        Err(ErrorResponse::from(ron_error(
+            StatusCode::UNAUTHORIZED,
+            super::NOT_AUTHORIZED_MESSAGE,
+        )))
+    } else {
+        Ok(())
+    }
+}
+
+pub 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,
+            super::NOT_AUTHORIZED_MESSAGE,
+        )))
+    } else {
+        Ok(())
+    }
+}
+
+pub async fn check_user_rights_recipe_ingredients(
+    connection: &db::Connection,
+    user: &Option<model::User>,
+    ingredient_ids: &[i64],
+) -> Result<()> {
+    if user.is_none()
+        || !connection
+            .can_edit_recipe_all_ingredients(user.as_ref().unwrap().id, ingredient_ids)
+            .await?
+    {
+        Err(ErrorResponse::from(ron_error(
+            StatusCode::UNAUTHORIZED,
+            super::NOT_AUTHORIZED_MESSAGE,
+        )))
+    } else {
+        Ok(())
+    }
+}
+
+pub async fn check_user_rights_shopping_list_entry(
+    connection: &db::Connection,
+    user: &Option<model::User>,
+    entry_id: i64,
+) -> Result<()> {
+    if user.is_none()
+        || !connection
+            .can_edit_shopping_list_entry(user.as_ref().unwrap().id, entry_id)
+            .await?
+    {
+        Err(ErrorResponse::from(ron_error(
+            StatusCode::UNAUTHORIZED,
+            super::NOT_AUTHORIZED_MESSAGE,
+        )))
+    } else {
+        Ok(())
+    }
+}
diff --git a/backend/src/services/ron/shopping_list.rs b/backend/src/services/ron/shopping_list.rs
new file mode 100644 (file)
index 0000000..4a785ee
--- /dev/null
@@ -0,0 +1,65 @@
+use axum::{
+    debug_handler,
+    extract::{Extension, State},
+    http::StatusCode,
+    response::{ErrorResponse, IntoResponse, Result},
+};
+use common::ron_api;
+
+use crate::{
+    data::db,
+    model,
+    ron_extractor::ExtractRon,
+    ron_utils::{ron_error, ron_response_ok},
+};
+
+use super::rights::*;
+
+impl From<model::ShoppingListItem> for common::ron_api::ShoppingListItem {
+    fn from(item: model::ShoppingListItem) -> Self {
+        Self {
+            id: item.id,
+            name: item.name,
+            quantity_value: item.quantity_value,
+            quantity_unit: item.quantity_unit,
+            recipe_id: item.recipe_id,
+            recipe_title: item.recipe_title,
+            date: item.date,
+            is_checked: item.is_checked,
+        }
+    }
+}
+
+#[debug_handler]
+pub async fn get(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+) -> Result<impl IntoResponse> {
+    if let Some(user) = user {
+        Ok(ron_response_ok(
+            connection
+                .get_shopping_list(user.id)
+                .await?
+                .into_iter()
+                .map(common::ron_api::ShoppingListItem::from)
+                .collect::<Vec<_>>(),
+        ))
+    } else {
+        Err(ErrorResponse::from(ron_error(
+            StatusCode::UNAUTHORIZED,
+            super::NOT_AUTHORIZED_MESSAGE,
+        )))
+    }
+}
+
+#[debug_handler]
+pub async fn set_entry_checked(
+    State(connection): State<db::Connection>,
+    Extension(user): Extension<Option<model::User>>,
+    ExtractRon(ron): ExtractRon<ron_api::Value<bool>>,
+) -> Result<impl IntoResponse> {
+    check_user_rights_shopping_list_entry(&connection, &user, ron.id).await?;
+    Ok(ron_response_ok(
+        connection.set_entry_checked(ron.id, ron.value).await?,
+    ))
+}
index 3bd50a8..426c9e3 100644 (file)
@@ -7,6 +7,9 @@
 
     <div id="shopping-list">
     </div>
+
+    <div id="shopping-list-checked">
+    </div>
 </div>
 
 <div id="hidden-templates">
index 0e308ce..50f9ef0 100644 (file)
@@ -19,6 +19,13 @@ pub struct Id {
     pub id: i64,
 }
 
+// A value associated with an id.
+#[derive(Serialize, Deserialize, Clone)]
+pub struct Value<T> {
+    pub id: i64,
+    pub value: T,
+}
+
 #[derive(Serialize, Deserialize, Clone)]
 pub struct Strings {
     pub strs: Vec<String>,
index 3a4f660..2d0737e 100644 (file)
@@ -2,6 +2,7 @@ use std::str::FromStr;
 
 use chrono::Locale;
 use common::{ron_api, utils::substitute_with_names};
+use futures::TryFutureExt;
 use gloo::events::EventListener;
 use wasm_bindgen::prelude::*;
 use wasm_bindgen_futures::spawn_local;
@@ -33,10 +34,14 @@ pub fn setup_page(is_user_logged: bool) -> Result<(), JsValue> {
     spawn_local(async move {
         let item_template: Element = selector("#hidden-templates .shopping-item");
         let container: Element = by_id("shopping-list");
+        let container_checked: Element = by_id("shopping-list-checked");
         let date_format =
             selector::<Element>("#hidden-templates .calendar-date-format").inner_html();
         for item in shopping_list.get_items().await.unwrap() {
             let item_element = item_template.deep_clone();
+
+            // item_element.set_id(format!("shopping-item-{}", ));
+
             item_element
                 .selector::<Element>(".item-name")
                 .set_inner_html(&item.name);
@@ -62,7 +67,48 @@ pub fn setup_page(is_user_logged: bool) -> Result<(), JsValue> {
                     .unwrap();
             }
 
-            container.append_child(&item_element).unwrap();
+            EventListener::new(
+                &item_element.selector(".item-is-checked"),
+                "change",
+                move |event| {
+                    let input: HtmlInputElement = event.target().unwrap().dyn_into().unwrap();
+                    spawn_local(async move {
+                        shopping_list
+                            .set_item_checked(item.id, input.checked())
+                            .await
+                            .unwrap();
+                        let item_element = input.parent_element().unwrap();
+                        item_element.remove();
+                        // TODO: Find the correct place to insert the element.
+                        if input.checked() {
+                            by_id::<Element>("shopping-list-checked")
+                                .append_child(&item_element)
+                                .unwrap();
+                        } else {
+                            by_id::<Element>("shopping-list")
+                                .append_child(&item_element)
+                                .unwrap();
+                        }
+                    });
+                },
+            )
+            .forget();
+
+            EventListener::new(&item_element, "click", move |event| {
+                let target: Element = event.target().unwrap().dyn_into().unwrap();
+
+                // if target.class_name() == "item-is-checked"
+            })
+            .forget();
+
+            if item.is_checked {
+                item_element
+                    .selector::<HtmlInputElement>(".item-is-checked")
+                    .set_checked(true);
+                container_checked.append_child(&item_element).unwrap();
+            } else {
+                container.append_child(&item_element).unwrap();
+            }
         }
     });
 
index 155a1d9..452c98a 100644 (file)
@@ -32,4 +32,20 @@ impl ShoppingList {
             Ok(request::get("shopping_list/get_list", ()).await?)
         }
     }
+
+    pub async fn set_item_checked(&self, item_id: i64, is_checked: bool) -> Result<()> {
+        if self.is_local {
+            todo!();
+        } else {
+            request::put(
+                "shopping_list/set_checked",
+                ron_api::Value {
+                    id: item_id,
+                    value: is_checked,
+                },
+            )
+            .await
+            .map_err(Error::from)
+        }
+    }
 }