Shopping list items can now be checked/unchecked

This commit is contained in:
Greg Burri 2025-02-12 23:13:48 +01:00
parent 3a3288bc93
commit a1fd63ad08
14 changed files with 940 additions and 790 deletions

20
Cargo.lock generated
View 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",

View 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#"

View 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)
}
}

View 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);

View file

@ -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")
}

View file

@ -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)
}

View file

@ -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")
}

View file

@ -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)
}

View file

@ -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(())
}
}

View file

@ -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?,
))
}

View file

@ -7,6 +7,9 @@
<div id="shopping-list">
</div>
<div id="shopping-list-checked">
</div>
</div>
<div id="hidden-templates">

View 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>,

View 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();
}
}
});

View 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)
}
}
}