diff --git a/cooked/src/routes.rs b/cooked/src/routes.rs index 4a93aed..ffb32fb 100644 --- a/cooked/src/routes.rs +++ b/cooked/src/routes.rs @@ -10,10 +10,13 @@ use actix_web::HttpMessage; use actix_web::{get, post, HttpRequest, HttpResponse, Responder}; use askama::Template; use askama_actix::TemplateToResponse; -use sea_orm::{prelude::*, DatabaseTransaction, QueryOrder, QuerySelect, TransactionTrait}; +use sea_orm::{ + prelude::*, DatabaseTransaction, JoinType, QueryOrder, QuerySelect, TransactionTrait, +}; use serde::Deserialize; -use std::collections::{BTreeMap, BTreeSet}; use serde_qs::actix::QsForm; +use std::collections::HashMap; +use std::collections::{BTreeMap, BTreeSet}; #[derive(Debug, Template, derive_more::Deref)] #[template(path = "recipe_card.jinja")] @@ -87,6 +90,30 @@ struct RecipeForm { page: Page, } +#[derive(Debug, Template, Deserialize, Clone, PartialEq)] +#[template(path = "recipies/update_form.jinja", ext = "html")] +struct EditRecipeForm { + id: i32, + title: String, + summary: String, + ingredients: String, + steps: String, + tags: String, + selected_tags: Vec, + image_url: String, + time: Option, + author: Option, + error: Option, + #[serde(default)] + known_tags: Vec, + #[serde(default)] + known_ingredients: Vec, + #[serde(default)] + session: Option, + #[serde(default)] + page: Page, +} + #[get("/sign-in")] async fn render_sign_in() -> SignInForm { SignInForm { @@ -445,9 +472,11 @@ async fn save_recipe(form: RecipeForm, t: &mut DatabaseTransaction) -> Result>(); + let selected_tags = form.selected_tags; + let selected_tags = selected_tags + .into_iter() + .filter_map(|s| s.parse().ok()) + .collect::>(); let text_tags = form .tags @@ -607,6 +636,139 @@ async fn save_recipe(form: RecipeForm, t: &mut DatabaseTransaction) -> Result, + admin: Identity, + db: Data, +) -> Result { + let id = id.into_inner(); + let recipe = match Recipies::find() + .filter(entities::recipies::Column::Id.eq(id)) + .one(&**db) + .await + { + Ok(Some(recipe)) => recipe, + Ok(_) => todo!("index & not found"), + Err(err) => { + todo!() + } + }; + let recipe_tags = RecipeTags::find() + .filter(entities::recipe_tags::Column::RecipeId.eq(id)) + .all(&**db) + .await + .unwrap_or_default() + .inspect_err(|e| tracing::error!("Failed to load recipe tags for edit recipe: {e}")) + .map_err(|_| Error::DatabaseError)?; + let tags = Tags::find() + .join( + JoinType::InnerJoin, + entities::tags::Relation::RecipeTags.def(), + ) + .filter(entities::recipe_tags::Column::RecipeId.eq(id)) + .all(&**db) + .await + .unwrap_or_default() + .inspect_err(|e| tracing::error!("Failed to load ingredients for edit recipe: {e}")) + .map_err(|_| Error::DatabaseError)?; + let recipe_ingredients = RecipeIngredients::find() + .filter(entities::recipe_ingredients::Column::RecipeId.eq(id)) + .all(&**db) + .await + .unwrap_or_default() + .inspect_err(|e| tracing::error!("Failed to load ingredients for edit recipe: {e}")) + .map_err(|_| Error::DatabaseError)?; + let ingredients = Ingredients::find() + .join_rev( + JoinType::InnerJoin, + entities::ingredients::Relation::RecipeIngredients.def(), + ) + .filter(entities::recipe_ingredients::Column::RecipeId.eq(id)) + .all(&**db) + .await + .unwrap_or_default() + .inspect_err(|e| tracing::error!("Failed to load ingredients for edit recipe: {e}")) + .map_err(|_| Error::DatabaseError)? + .into_iter() + .fold(HashMap::new(), |mut agg, ing| { + agg.insert(ing.id, ing); + agg + }); + Ok(EditRecipeForm { + id, + title: recipe.title, + summary: recipe.summary, + ingredients: recipe_ingredients + .into_iter() + .filter_map(|ingredient| { + format!( + "{} {} {}", + ingredient.qty, + ingredient.unit, + ingredients.get(&ingredient.id)?.name + ) + }) + .collect::>() + .join("\n"), + steps, + tags, + selected_tags, + image_url: recipe.image_url, + time: recipe.time, + author: recipe.author, + error: None, + known_tags: recipe_tags.iter().map(|tag| tag.tag_id)).collect(), + known_ingredients: recipe_ingredients.iter().map(|ing| ing.ingredient_id).collect(), + session: admin.id(), + page: Page::Index, + }) +} + +#[post("/recipe/{id}/edit")] +async fn update_recipe( + id: actix_web::web::Path, + admin: Identity, + db: Data, + form: QsForm, +) -> Result { + let mut form = form.into_inner(); + form.session = admin.id().ok(); + form.page = Page::Index; + + let mut failure = form.clone(); + + let mut t = db + .begin() + .await + .inspect_err(|e| tracing::error!("Create recipe transaction: {e}")) + .map_err(|_| Error::DatabaseError)?; + match save_recipe(form, &mut t).await { + Err(e) => { + failure.error = Some(e.to_string()); + let _ = t.rollback().await; + failure.known_tags = Tags::find().all(&**db).await.unwrap_or_default(); + failure.known_ingredients = Ingredients::find().all(&**db).await.unwrap_or_default(); + Ok(failure.to_response()) + } + Ok(recipe_id) => { + let _ = t.commit().await; + let Ok(_) = tokio::fs::copy( + format!("/tmp{}", failure.image_url), + format!(".{}", failure.image_url), + ) + .await + .inspect_err(|e| tracing::error!("Failed to copy {}: {e}", failure.image_url)) else { + tracing::error!("Failed to copy file: {}", failure.image_url); + return Ok(HttpResponse::InternalServerError().finish()); + }; + Ok(HttpResponse::SeeOther() + .append_header(("location", format!("/recipe/{recipe_id}").as_str())) + .finish()) + } + } +} + #[get("/styles.css")] async fn styles_css() -> HttpResponse { HttpResponse::Ok() @@ -651,6 +813,8 @@ pub fn configure(config: &mut actix_web::web::ServiceConfig) { .service(show) .service(search_page) .service(search_results) + .service(update_recipe) + .service(edit_recipe) .service(recipe_form) .service(create_recipe) .service(recipe_image_upload) diff --git a/cooked/templates/recipies/update_form.jinja b/cooked/templates/recipies/update_form.jinja index f99462d..70bbf6f 100644 --- a/cooked/templates/recipies/update_form.jinja +++ b/cooked/templates/recipies/update_form.jinja @@ -36,10 +36,10 @@

Summary supports Markdown

-