Recipe edit (WIP): add API to set some recipe values

This commit is contained in:
Greg Burri 2024-12-23 01:37:01 +01:00
parent c6dfff065c
commit dd05a673d9
20 changed files with 690 additions and 2189 deletions

View file

@ -1,13 +1,46 @@
use super::{Connection, DBError, Result};
use crate::{
consts,
data::model::{self, Difficulty},
};
use crate::{consts, data::model};
use common::ron_api::Difficulty;
impl Connection {
pub async fn get_all_recipe_titles(&self) -> Result<Vec<(i64, String)>> {
sqlx::query_as("SELECT [id], [title] FROM [Recipe] ORDER BY [title]")
.fetch_all(&self.pool)
pub async fn get_all_published_recipe_titles(&self) -> Result<Vec<(i64, String)>> {
sqlx::query_as(
r#"
SELECT [id], [title]
FROM [Recipe]
WHERE [is_published] = true
ORDER BY [title]
"#,
)
.fetch_all(&self.pool)
.await
.map_err(DBError::from)
}
pub async fn get_all_unpublished_recipe_titles(
&self,
owned_by: i64,
) -> Result<Vec<(i64, String)>> {
sqlx::query_as(
r#"
SELECT [id], [title]
FROM [Recipe]
WHERE [is_published] = false AND [user_id] = $1
ORDER BY [title]
"#,
)
.bind(owned_by)
.fetch_all(&self.pool)
.await
.map_err(DBError::from)
}
pub async fn can_edit_recipe(&self, user_id: i64, recipe_id: i64) -> Result<bool> {
sqlx::query_scalar(r#"SELECT COUNT(*) FROM [Recipe] WHERE [id] = $1 AND [user_id] = $2"#)
.bind(recipe_id)
.bind(user_id)
.fetch_one(&self.pool)
.await
.map_err(DBError::from)
}

View file

@ -1,4 +1,5 @@
use chrono::prelude::*;
use common::ron_api::Difficulty;
use sqlx::{self, FromRow};
#[derive(Debug, Clone, FromRow)]
@ -50,29 +51,3 @@ pub struct Ingredient {
pub quantity: i32,
pub quantity_unit: String,
}
#[derive(PartialEq, Debug)]
pub enum Difficulty {
Unknown = 0,
Easy = 1,
Medium = 2,
Hard = 3,
}
impl TryFrom<u32> for Difficulty {
type Error = &'static str;
fn try_from(value: u32) -> Result<Self, Self::Error> {
Ok(match value {
1 => Self::Easy,
2 => Self::Medium,
3 => Self::Hard,
_ => Self::Unknown,
})
}
}
impl From<Difficulty> for u32 {
fn from(value: Difficulty) -> Self {
value as u32
}
}

View file

@ -3,10 +3,17 @@ use askama::Template;
use crate::data::model;
pub struct Recipes {
pub list: Vec<(i64, String)>,
pub published: Vec<(i64, String)>,
pub unpublished: Vec<(i64, String)>,
pub current_id: Option<i64>,
}
impl Recipes {
pub fn is_current(&self, id: &&i64) -> bool {
self.current_id == Some(**id)
}
}
#[derive(Template)]
#[template(path = "home.html")]
pub struct HomeTemplate {
@ -111,3 +118,10 @@ pub struct RecipeEditTemplate {
pub recipe: model::Recipe,
pub languages: [(&'static str, &'static str); 2],
}
#[derive(Template)]
#[template(path = "recipes_list_fragment.html")]
pub struct RecipesListFragmentTemplate {
pub user: Option<model::User>,
pub recipes: Recipes,
}

View file

@ -5,7 +5,7 @@ use axum::{
http::StatusCode,
middleware::{self, Next},
response::{Response, Result},
routing::get,
routing::{get, put},
Router,
};
use axum_extra::extract::cookie::CookieJar;
@ -86,8 +86,28 @@ async fn main() {
let ron_api_routes = Router::new()
// Disabled: update user profile is now made with a post data ('edit_user_post').
// .route("/user/update", put(services::ron::update_user))
.route("/recipe/set_title", put(services::ron::set_recipe_title))
.route(
"/recipe/set_description",
put(services::ron::set_recipe_description),
)
.route(
"/recipe/set_estimated_time",
put(services::ron::set_estimated_time),
)
.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),
)
.fallback(services::ron::not_found);
let fragments_routes = Router::new().route(
"/recipes_list",
get(services::fragments::recipes_list_fragments),
);
let html_routes = Router::new()
.route("/", get(services::home_page))
.route(
@ -123,6 +143,7 @@ async fn main() {
let app = Router::new()
.merge(html_routes)
.nest("/fragments", fragments_routes)
.nest("/ron-api", ron_api_routes)
.fallback(services::not_found)
.layer(TraceLayer::new_for_http())

View file

@ -13,6 +13,7 @@ use crate::{
ron_utils,
};
pub mod fragments;
pub mod recipe;
pub mod ron;
pub mod user;
@ -46,15 +47,19 @@ pub async fn home_page(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
) -> Result<impl IntoResponse> {
let recipes = connection.get_all_recipe_titles().await?;
Ok(HomeTemplate {
user,
recipes: Recipes {
list: recipes,
current_id: None,
let recipes = Recipes {
published: connection.get_all_published_recipe_titles().await?,
unpublished: if let Some(user) = user.as_ref() {
connection
.get_all_unpublished_recipe_titles(user.id)
.await?
} else {
vec![]
},
})
current_id: None,
};
Ok(HomeTemplate { user, recipes })
}
///// 404 /////

View file

@ -0,0 +1,31 @@
use axum::{
debug_handler,
extract::{Extension, State},
response::{IntoResponse, Result},
};
// use tracing::{event, Level};
use crate::{
data::{db, model},
html_templates::*,
};
#[debug_handler]
pub async fn recipes_list_fragments(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
) -> Result<impl IntoResponse> {
let recipes = Recipes {
published: connection.get_all_published_recipe_titles().await?,
unpublished: if let Some(user) = user.as_ref() {
connection
.get_all_unpublished_recipe_titles(user.id)
.await?
} else {
vec![]
},
current_id: None,
};
Ok(RecipesListFragmentTemplate { user, recipes })
}

View file

@ -35,18 +35,23 @@ pub async fn edit_recipe(
if let Some(user) = user {
let recipe = connection.get_recipe(recipe_id).await?.unwrap();
if recipe.user_id == user.id {
let recipes = Recipes {
published: connection.get_all_published_recipe_titles().await?,
unpublished: connection
.get_all_unpublished_recipe_titles(user.id)
.await?,
current_id: Some(recipe_id),
};
Ok(RecipeEditTemplate {
user: Some(user),
recipes: Recipes {
list: connection.get_all_recipe_titles().await?,
current_id: Some(recipe_id),
},
recipes,
recipe,
languages: consts::LANGUAGES,
}
.into_response())
} else {
Ok(MessageTemplate::new("Unable to edit this recipe").into_response())
Ok(MessageTemplate::new("Not allowed to edit this recipe").into_response())
}
} else {
Ok(MessageTemplate::new("Not logged in").into_response())
@ -59,17 +64,37 @@ pub async fn view(
Extension(user): Extension<Option<model::User>>,
Path(recipe_id): Path<i64>,
) -> Result<Response> {
let recipes = connection.get_all_recipe_titles().await?;
match connection.get_recipe(recipe_id).await? {
Some(recipe) => Ok(RecipeViewTemplate {
user,
recipes: Recipes {
list: recipes,
current_id: Some(recipe.id),
},
recipe,
Some(recipe) => {
if !recipe.is_published
&& (user.is_none() || recipe.user_id != user.as_ref().unwrap().id)
{
return Ok(MessageTemplate::new_with_user(
&format!("Not allowed the view the recipe {}", recipe_id),
user,
)
.into_response());
}
let recipes = Recipes {
published: connection.get_all_published_recipe_titles().await?,
unpublished: if let Some(user) = user.as_ref() {
connection
.get_all_unpublished_recipe_titles(user.id)
.await?
} else {
vec![]
},
current_id: Some(recipe_id),
};
Ok(RecipeViewTemplate {
user,
recipes,
recipe,
}
.into_response())
}
.into_response()),
None => Ok(MessageTemplate::new_with_user(
&format!("Cannot find the recipe {}", recipe_id),
user,

View file

@ -81,6 +81,103 @@ pub async fn update_user(
Ok(StatusCode::OK)
}
async fn check_user_rights(
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,
"Action not authorized",
)))
} else {
Ok(())
}
}
#[debug_handler]
pub async fn set_recipe_title(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeTitle>,
) -> Result<StatusCode> {
check_user_rights(&connection, &user, ron.recipe_id).await?;
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(&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_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(&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 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(&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> {
check_user_rights(&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(&connection, &user, ron.recipe_id).await?;
connection
.set_recipe_is_published(ron.recipe_id, ron.is_published)
.await?;
Ok(StatusCode::OK)
}
///// 404 /////
#[debug_handler]
pub async fn not_found(Extension(_user): Extension<Option<model::User>>) -> impl IntoResponse {