diff --git a/cooked/src/routes.rs b/cooked/src/routes.rs index e23599e..2f03a12 100644 --- a/cooked/src/routes.rs +++ b/cooked/src/routes.rs @@ -80,6 +80,8 @@ struct RecipeForm { #[serde(default)] known_tags: Vec, #[serde(default)] + known_ingredients: Vec, + #[serde(default)] session: Option, #[serde(default)] page: Page, @@ -326,23 +328,26 @@ async fn show( }) .collect::>(); - HttpResponse::Ok().body( - RecipeDetailTemplate { - steps, - tags, - recipe, - ingredients, - session: admin.and_then(|s| s.id().ok()), - page: Page::Recipe, - } - .render() - .unwrap_or_default(), - ) + HttpResponse::Ok() + .append_header((CONTENT_TYPE, "text/html")) + .body( + RecipeDetailTemplate { + steps, + tags, + recipe, + ingredients, + session: admin.and_then(|s| s.id().ok()), + page: Page::Recipe, + } + .render() + .unwrap_or_default(), + ) } #[get("/create")] async fn recipe_form(admin: Identity, db: Data) -> RecipeForm { let tags = Tags::find().all(&**db).await.unwrap_or_default(); + let ingredients = Ingredients::find().all(&**db).await.unwrap_or_default(); RecipeForm { title: "".into(), summary: "".into(), @@ -355,6 +360,7 @@ async fn recipe_form(admin: Identity, db: Data) -> RecipeFor time: None, error: None, known_tags: tags, + known_ingredients: ingredients, session: admin.id().ok(), page: Page::Index, } @@ -363,7 +369,7 @@ async fn recipe_form(admin: Identity, db: Data) -> RecipeFor #[post("/create")] async fn create_recipe( admin: Identity, - form: Form, + form: actix_web::web::Json, db: Data, ) -> Result { let mut form = form.into_inner(); @@ -382,9 +388,10 @@ async fn create_recipe( 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(_) => { + Ok(recipe_id) => { let _ = t.commit().await; let Ok(_) = tokio::fs::copy( format!("/tmp{}", failure.image_url), @@ -395,14 +402,14 @@ async fn create_recipe( tracing::error!("Failed to copy file: {}", failure.image_url); return Ok(HttpResponse::InternalServerError().finish()); }; - Ok(HttpResponse::TemporaryRedirect() - .append_header(("location", "/")) + Ok(HttpResponse::SeeOther() + .append_header(("location", format!("/recipe/{recipe_id}").as_str())) .finish()) } } } -async fn save_recipe(form: RecipeForm, t: &mut DatabaseTransaction) -> Result<(), Error> { +async fn save_recipe(form: RecipeForm, t: &mut DatabaseTransaction) -> Result { use crate::entities::recipies::ActiveModel as RAM; use sea_orm::ActiveValue::*; @@ -434,85 +441,168 @@ async fn save_recipe(form: RecipeForm, t: &mut DatabaseTransaction) -> Result<() .map_err(|_| Error::SaveRecipe)? .last_insert_id; - let steps = crate::utils::parse_steps(&form.steps)? - .into_iter() - .map(|(body, hint)| entities::recipe_steps::ActiveModel { - body: Set(body), - hint: Set(hint), - recipe_id: Set(recipe_id), - ..Default::default() - }) - .collect::>(); - let _steps = RecipeSteps::insert_many(steps) - .exec(&mut *t) - .await - .inspect_err(|e| tracing::error!("Save steps: {e}")) - .map_err(|_| Error::SaveRecipeStep)?; - let ingredients = crate::utils::parse_ingenedients(&form.ingeredients)?; - let known = Ingredients::find() - .filter( - entities::ingredients::Column::Name.is_in( - ingredients - .iter() - .map(|(name, ..)| name.to_string()) - .collect::>(), - ), - ) - .all(&mut *t) - .await - .inspect_err(|e| tracing::warn!("Failed to find ingredients: {e}")) - .map_err(|_| Error::SaveRecipeIngeredient)?; - let missing = { - let known = known - .iter() - .map(|model| &model.name) - .collect::>(); - let missing = ingredients - .iter() - .filter(|(name, ..)| !known.contains(name)) - .map(|(name, ..)| entities::ingredients::ActiveModel { - name: Set(name.to_owned()), + // Tags + { + tracing::debug!("Selected tags: {:?}", form.selected_tags); + let selected_tags = form + .selected_tags; + + let text_tags = form + .tags + .split(",") + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(String::from) + .collect::>(); + tracing::debug!("Received tags: {text_tags:?}"); + let known = Tags::find() + .filter(entities::tags::Column::Name.is_in(&text_tags)) + .all(&mut *t) + .await + .unwrap_or_default(); + tracing::debug!("Known tags: {known:?}"); + let missing = { + let known = known + .iter() + .map(|model| &model.name) + .collect::>(); + tracing::debug!("Known tag names: {known:?}"); + let mut missing = Vec::with_capacity(text_tags.len()); + for tag_name in text_tags + .into_iter() + .filter(|tag_name| !known.contains(&tag_name)) + { + tracing::debug!("Creating missing tag: {tag_name:?}"); + missing.push( + Tags::insert(entities::tags::ActiveModel { + name: Set(tag_name), + ..Default::default() + }) + .exec_with_returning(&mut *t) + .await + .inspect_err(|e| tracing::warn!("Failed to find tag: {e}")) + .map_err(|_| Error::SaveTag)?, + ); + } + tracing::debug!("Missing tags: {missing:?}"); + missing + }; + #[cfg(debug_assertions)] + tracing::debug!( + "Tags to attach: {:?}", + known + .clone() + .into_iter() + .map(|tag| tag.id) + .chain(missing.clone().into_iter().map(|tag| tag.id)) + .chain(selected_tags.clone().into_iter()) + .collect::>() + ); + for tag_id in known + .into_iter() + .map(|tag| tag.id) + .chain(missing.into_iter().map(|tag| tag.id)) + .chain(selected_tags.into_iter()) + .collect::>() + { + RecipeTags::insert(entities::recipe_tags::ActiveModel { + recipe_id: Set(recipe_id), + tag_id: Set(tag_id), + ..Default::default() + }) + .exec(&mut *t) + .await + .inspect_err(|e| tracing::error!("Failed to save RecipeTag: {e}")) + .map_err(|_| Error::SaveRecipeTag)?; + } + } + + // Steps + { + let steps = crate::utils::parse_steps(&form.steps)? + .into_iter() + .map(|(body, hint)| entities::recipe_steps::ActiveModel { + body: Set(body), + hint: Set(hint), + recipe_id: Set(recipe_id), ..Default::default() }) .collect::>(); - let mut v = Vec::with_capacity(missing.len()); - for missing in missing { - v.push( - Ingredients::insert(missing) - .exec_with_returning(&mut *t) - .await - .inspect_err(|e| tracing::error!("Failed to create ingredients: {e}")) - .map_err(|_| Error::DatabaseError)?, - ) - } - v - }; - let map = known - .into_iter() - .chain(missing.into_iter()) - .map(|row| (row.name, row.id)) - .collect::>(); + let _steps = RecipeSteps::insert_many(steps) + .exec(&mut *t) + .await + .inspect_err(|e| tracing::error!("Save steps: {e}")) + .map_err(|_| Error::SaveRecipeStep)?; + } - RecipeIngredients::insert_many( - ingredients - .into_iter() - .filter_map(|(name, unit, qty)| { - Some(entities::recipe_ingredients::ActiveModel { - ingredient_id: Set(*map.get(&name)?), - qty: Set(qty), - unit: Set(unit.unwrap_or_default()), - recipe_id: Set(recipe_id), + // Ingredients + { + let ingredients = crate::utils::parse_ingenedients(&form.ingeredients)?; + let known = Ingredients::find() + .filter( + entities::ingredients::Column::Name.is_in( + ingredients + .iter() + .map(|(name, ..)| name.to_string()) + .collect::>(), + ), + ) + .all(&mut *t) + .await + .inspect_err(|e| tracing::warn!("Failed to find ingredients: {e}")) + .map_err(|_| Error::SaveRecipeIngeredient)?; + let missing = { + let known = known + .iter() + .map(|model| &model.name) + .collect::>(); + let missing = ingredients + .iter() + .filter(|(name, ..)| !known.contains(name)) + .map(|(name, ..)| entities::ingredients::ActiveModel { + name: Set(name.to_owned()), ..Default::default() }) - }) - .collect::>(), - ) - .exec(&mut *t) - .await - .inspect_err(|e| tracing::error!("Save ingeredients: {e}")) - .map_err(|_| Error::SaveRecipeIngeredient)?; + .collect::>(); + let mut v = Vec::with_capacity(missing.len()); + for missing in missing { + v.push( + Ingredients::insert(missing) + .exec_with_returning(&mut *t) + .await + .inspect_err(|e| tracing::error!("Failed to create ingredients: {e}")) + .map_err(|_| Error::DatabaseError)?, + ) + } + v + }; + let map = known + .into_iter() + .chain(missing.into_iter()) + .map(|row| (row.name, row.id)) + .collect::>(); - Ok(()) + RecipeIngredients::insert_many( + ingredients + .into_iter() + .filter_map(|(name, unit, qty)| { + Some(entities::recipe_ingredients::ActiveModel { + ingredient_id: Set(*map.get(&name)?), + qty: Set(qty), + unit: Set(unit.unwrap_or_default()), + recipe_id: Set(recipe_id), + ..Default::default() + }) + }) + .collect::>(), + ) + .exec(&mut *t) + .await + .inspect_err(|e| tracing::error!("Save ingeredients: {e}")) + .map_err(|_| Error::SaveRecipeIngeredient)?; + }; + + Ok(recipe_id) } #[get("/styles.css")] @@ -526,7 +616,7 @@ async fn tmp_cleanup() { let d = tokio::time::Duration::from_secs(10); loop { tokio::time::sleep(d).await; - tracing::info!("Starting files cleanup"); + // tracing::info!("Starting files cleanup"); let Ok(mut dir) = tokio::fs::read_dir("/tmp/assets/recipies/images").await else { tracing::info!("Files cleanup failed. No tmp images dir"); @@ -545,7 +635,7 @@ async fn tmp_cleanup() { let _ = tokio::fs::remove_file(entry.path()).await; } } - tracing::info!("Files cleanup done"); + // tracing::info!("Files cleanup done"); } } diff --git a/cooked/src/types.rs b/cooked/src/types.rs index e5c5a1e..729c088 100644 --- a/cooked/src/types.rs +++ b/cooked/src/types.rs @@ -27,6 +27,8 @@ pub enum Error { #[error("Internal server error")] SaveRecipeIngeredient, #[error("Internal server error")] + SaveTag, + #[error("Internal server error")] SaveRecipeTag, #[error("Internal server error")] DatabaseError, @@ -42,6 +44,7 @@ impl actix_web::error::ResponseError for Error { Self::SaveRecipeStep => StatusCode::INTERNAL_SERVER_ERROR, Self::SaveRecipeIngeredient => StatusCode::INTERNAL_SERVER_ERROR, Self::SaveRecipeTag => StatusCode::INTERNAL_SERVER_ERROR, + Self::SaveTag => StatusCode::INTERNAL_SERVER_ERROR, Self::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, } } diff --git a/cooked/templates/recipies/create_form.jinja b/cooked/templates/recipies/create_form.jinja index 64a0829..6043f58 100644 --- a/cooked/templates/recipies/create_form.jinja +++ b/cooked/templates/recipies/create_form.jinja @@ -42,6 +42,13 @@ +
+ {% for ingredient in known_ingredients %} + + {% endfor %} +
Ingeredients should be listed as list of AMOUNT UNIT INGEREDIENT. Example: @@ -79,9 +86,9 @@ {{ tag.name }} {% match (tag.id, selected_tags.as_slice())|is_checked %} {% when true %} - + {% when false %} - + {% endmatch %} {% endfor %} @@ -95,7 +102,8 @@ {% endblock %} diff --git a/migration/src/m20220101_000001_create_table.rs b/migration/src/m20220101_000001_create_table.rs index 3e5fff2..d394e10 100644 --- a/migration/src/m20220101_000001_create_table.rs +++ b/migration/src/m20220101_000001_create_table.rs @@ -87,7 +87,9 @@ impl MigrationTrait for Migration { .to_tbl(Recipe::Recipies) .to_col(Recipe::Id) .from_tbl(RecipeStep::RecipeSteps) - .from_col(RecipeStep::RecipeId), + .from_col(RecipeStep::RecipeId) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), ) .to_owned(), ) @@ -122,14 +124,18 @@ impl MigrationTrait for Migration { .to_tbl(Recipe::Recipies) .to_col(Recipe::Id) .from_tbl(RecipeIngredient::RecipeIngredients) - .from_col(RecipeIngredient::RecipeId), + .from_col(RecipeIngredient::RecipeId) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), ) .foreign_key( &mut ForeignKeyCreateStatement::new() .to_tbl(Ingredient::Ingredients) .to_col(Recipe::Id) .from_tbl(RecipeIngredient::RecipeIngredients) - .from_col(RecipeIngredient::IngredientId), + .from_col(RecipeIngredient::IngredientId) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), ) .to_owned(), ) @@ -154,14 +160,18 @@ impl MigrationTrait for Migration { .to_tbl(Recipe::Recipies) .to_col(Recipe::Id) .from_tbl(RecipeTag::RecipeTags) - .from_col(RecipeTag::RecipeId), + .from_col(RecipeTag::RecipeId) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), ) .foreign_key( &mut ForeignKeyCreateStatement::new() .to_tbl(Tag::Tags) .to_col(Tag::Id) .from_tbl(RecipeTag::RecipeTags) - .from_col(RecipeTag::TagId), + .from_col(RecipeTag::TagId) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), ) .to_owned(), )