Recipe edit (WIP): add API to set some recipe values
This commit is contained in:
parent
c6dfff065c
commit
dd05a673d9
20 changed files with 690 additions and 2189 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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 /////
|
||||
|
|
|
|||
31
backend/src/services/fragments.rs
Normal file
31
backend/src/services/fragments.rs
Normal 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 })
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue