diff --git a/cooked/src/routes.rs b/cooked/src/routes.rs
index 7fbc200..9a5c066 100644
--- a/cooked/src/routes.rs
+++ b/cooked/src/routes.rs
@@ -10,6 +10,7 @@ use actix_web::HttpMessage;
use actix_web::{get, post, HttpRequest, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
+use itertools::Itertools;
use sea_orm::{
prelude::*, DatabaseTransaction, JoinType, QueryOrder, QuerySelect, TransactionTrait,
};
@@ -22,14 +23,249 @@ use std::collections::{BTreeMap, BTreeSet};
#[template(path = "recipe_card.jinja")]
struct RecipeCard(entities::recipies::Model);
+#[derive(Debug, Template, derive_more::Deref)]
+#[template(path = "ingredient_card.jinja")]
+struct IngredientCard(crate::types::ingredient_with_recipe_count::Model);
+
+#[derive(Debug, Template, derive_more::Deref)]
+#[template(path = "tag_card.jinja")]
+struct TagCard(crate::types::tag_with_recipe_count::Model);
+
#[derive(Debug, Template)]
-#[template(path = "index.jinja", ext = "html")]
-struct IndexTemplate {
+#[template(path = "ingredient.jinja", ext = "html")]
+pub struct IngredientTemplate {
recipies: Vec,
count: u64,
session: Option,
page: Page,
}
+impl IngredientTemplate {
+ pub async fn load(
+ db: &DatabaseConnection,
+ ingredient_id: u32,
+ page: Option,
+ session: Option,
+ ) -> Self {
+ let count = (entities::prelude::Recipies::find()
+ .join(
+ JoinType::InnerJoin,
+ entities::recipies::Relation::RecipeIngredients.def(),
+ )
+ .filter(entities::recipe_ingredients::Column::IngredientId.eq(ingredient_id))
+ .count(&*db)
+ .await
+ .unwrap_or_default() as f64
+ / 20.0)
+ .ceil() as u64;
+ let recipies = entities::prelude::Recipies::find()
+ .join(
+ JoinType::InnerJoin,
+ entities::recipies::Relation::RecipeIngredients.def(),
+ )
+ .filter(entities::recipe_ingredients::Column::IngredientId.eq(ingredient_id))
+ .order_by_desc(entities::recipies::Column::Title)
+ .limit(20)
+ .offset(page.unwrap_or_default() as u64)
+ .all(&*db)
+ .await
+ .unwrap_or_default();
+
+ Self {
+ recipies: recipies.into_iter().map(RecipeCard).collect(),
+ count,
+ session,
+ page: Page::Index,
+ }
+ }
+}
+
+#[derive(Debug, Template)]
+#[template(path = "ingredients.jinja", ext = "html")]
+pub struct IngredientsTemplate {
+ session: Option,
+ page: Page,
+ ingredients: Vec,
+}
+
+impl IngredientsTemplate {
+ pub async fn load(db: &DatabaseConnection, session: Option) -> Self {
+ let rel = || {
+ Ingredients::find()
+ .join(
+ JoinType::Join,
+ entities::ingredients::Relation::RecipeIngredients.def(),
+ )
+ .join(
+ JoinType::Join,
+ entities::recipe_ingredients::Relation::Recipies.def(),
+ )
+ };
+ let ingredients = rel()
+ .select_only()
+ .column(entities::ingredients::Column::Id)
+ .column(entities::ingredients::Column::Name)
+ .column_as(
+ entities::recipe_ingredients::Column::RecipeId.count(),
+ "recipe_count",
+ )
+ .order_by_asc(entities::ingredients::Column::Name)
+ .group_by(entities::ingredients::Column::Id)
+ .into_model::()
+ .all(&*db)
+ .await
+ .inspect_err(|e| tracing::error!("Failed to load ingredients: {e}"))
+ .unwrap_or_default()
+ .into_iter()
+ .map(IngredientCard)
+ .collect();
+ Self {
+ ingredients,
+ session,
+ page: Page::Index,
+ }
+ }
+}
+
+#[derive(Debug, Template)]
+#[template(path = "tags.jinja", ext = "html")]
+pub struct TagsTemplate {
+ session: Option,
+ page: Page,
+ tags: Vec,
+}
+
+impl TagsTemplate {
+ pub async fn load(db: &DatabaseConnection, session: Option) -> Self {
+ let rel = || {
+ Tags::find()
+ .join(JoinType::Join, entities::tags::Relation::RecipeTags.def())
+ .join(
+ JoinType::Join,
+ entities::recipe_tags::Relation::Recipies.def(),
+ )
+ };
+ let tags = rel()
+ .select_only()
+ .column(entities::tags::Column::Id)
+ .column(entities::tags::Column::Name)
+ .column_as(
+ entities::recipe_tags::Column::RecipeId.count(),
+ "recipe_count",
+ )
+ .order_by_asc(entities::tags::Column::Name)
+ .group_by(entities::tags::Column::Id)
+ .into_model::()
+ .all(&*db)
+ .await
+ .inspect_err(|e| tracing::error!("Failed to load ingredients: {e}"))
+ .unwrap_or_default()
+ .into_iter()
+ .map(TagCard)
+ .collect();
+ Self {
+ tags,
+ session,
+ page: Page::Index,
+ }
+ }
+}
+//
+//
+//
+//
+
+#[derive(Debug, Template)]
+#[template(path = "tag.jinja", ext = "html")]
+pub struct TagTemplate {
+ recipies: Vec,
+ count: u64,
+ session: Option,
+ page: Page,
+}
+impl TagTemplate {
+ pub async fn load(
+ db: &DatabaseConnection,
+ ingredient_id: u32,
+ page: Option,
+ session: Option,
+ ) -> Self {
+ let count = (entities::prelude::Recipies::find()
+ .join(
+ JoinType::InnerJoin,
+ entities::recipies::Relation::RecipeTags.def(),
+ )
+ .filter(entities::recipe_tags::Column::TagId.eq(ingredient_id))
+ .count(&*db)
+ .await
+ .unwrap_or_default() as f64
+ / 20.0)
+ .ceil() as u64;
+ let recipies = entities::prelude::Recipies::find()
+ .join(
+ JoinType::InnerJoin,
+ entities::recipies::Relation::RecipeTags.def(),
+ )
+ .filter(entities::recipe_tags::Column::TagId.eq(ingredient_id))
+ .order_by_desc(entities::recipies::Column::Title)
+ .limit(20)
+ .offset(page.unwrap_or_default() as u64)
+ .all(&*db)
+ .await
+ .unwrap_or_default();
+
+ Self {
+ recipies: recipies.into_iter().map(RecipeCard).collect(),
+ count,
+ session,
+ page: Page::Index,
+ }
+ }
+}
+//
+//
+//
+//
+
+#[derive(Debug, Template)]
+#[template(path = "index.jinja", ext = "html")]
+pub struct IndexTemplate {
+ recipies: Vec,
+ count: u64,
+ session: Option,
+ page: Page,
+ error: Option,
+}
+
+impl IndexTemplate {
+ pub async fn load(db: &DatabaseConnection, page: Option, session: Option) -> Self {
+ let count = (entities::prelude::Recipies::find()
+ .count(&*db)
+ .await
+ .unwrap_or_default() as f64
+ / 20.0)
+ .ceil() as u64;
+ let recipies = entities::prelude::Recipies::find()
+ .order_by_desc(entities::recipies::Column::Title)
+ .limit(20)
+ .offset(page.unwrap_or_default() as u64)
+ .all(&*db)
+ .await
+ .unwrap_or_default();
+
+ Self {
+ recipies: recipies.into_iter().map(RecipeCard).collect(),
+ count,
+ session,
+ page: Page::Index,
+ error: None,
+ }
+ }
+
+ pub fn with_error(mut self, error: String) -> Self {
+ self.error = Some(error);
+ self
+ }
+}
#[derive(Debug, Template)]
#[template(path = "search.jinja", ext = "html")]
@@ -72,10 +308,10 @@ struct ImageUpload {
struct RecipeForm {
title: String,
summary: String,
- ingeredients: String,
+ ingredients: String,
steps: String,
tags: String,
- selected_tags: Vec,
+ selected_tags: Option>,
image_url: String,
time: Option,
author: Option,
@@ -99,8 +335,7 @@ struct EditRecipeForm {
ingredients: String,
steps: String,
tags: String,
- selected_tags: Vec,
- image_url: String,
+ selected_tags: Option>,
time: Option,
author: Option,
error: Option,
@@ -161,10 +396,6 @@ async fn sign_in(
.any(|admin| admin.email == payload.email && admin.pass == payload.password);
if is_admin {
tracing::info!("Valid credentials");
- // let res = session
- // .insert(SESSION_KEY, payload.email.clone())
- // .inspect_err(|e| tracing::error!("Failed to save session: {e}"));
- // tracing::debug!("Saving session res: {res:?}");
let _s =
Identity::login(&req.extensions(), payload.email).expect("Failed to store session");
@@ -256,33 +487,55 @@ async fn index_html(
q: actix_web::web::Query,
admin: Option,
) -> HttpResponse {
- let count = (entities::prelude::Recipies::find()
- .count(&**db)
+ IndexTemplate::load(&**db, q.page, admin.and_then(|session| session.id().ok()))
.await
- .unwrap_or_default() as f64
- / 20.0)
- .ceil() as u64;
- let recipies = entities::prelude::Recipies::find()
- .order_by_desc(entities::recipies::Column::Title)
- .limit(20)
- .offset(q.page.unwrap_or_default() as u64)
- .all(&**db)
+ .to_response()
+}
+
+#[get("/ingredients")]
+async fn ingredients_list(db: Data, admin: Option) -> HttpResponse {
+ IngredientsTemplate::load(&**db, admin.and_then(|session| session.id().ok()))
.await
- .unwrap_or_default();
+ .to_response()
+}
+#[get("/tags")]
+async fn tags_list(db: Data, admin: Option) -> HttpResponse {
+ TagsTemplate::load(&**db, admin.and_then(|session| session.id().ok()))
+ .await
+ .to_response()
+}
+#[get("/tags/{ingredient}")]
+async fn by_tag(
+ db: Data,
+ q: actix_web::web::Query,
+ admin: Option,
+ path: Path,
+) -> HttpResponse {
+ TagTemplate::load(
+ &**db,
+ path.into_inner(),
+ q.page,
+ admin.and_then(|session| session.id().ok()),
+ )
+ .await
+ .to_response()
+}
- let html = IndexTemplate {
- recipies: recipies.into_iter().map(RecipeCard).collect(),
- count,
- session: admin.and_then(|s| s.id().ok()),
- page: Page::Index,
- }
- .render()
- .unwrap();
-
- HttpResponse::Ok()
- .append_header((CONTENT_TYPE, "text/html; charset=utf-8"))
- .append_header((CONTENT_LENGTH, html.len()))
- .body(html)
+#[get("/ingredients/{ingredient}")]
+async fn by_ingredient(
+ db: Data,
+ q: actix_web::web::Query,
+ admin: Option,
+ path: Path,
+) -> HttpResponse {
+ IngredientTemplate::load(
+ &**db,
+ path.into_inner(),
+ q.page,
+ admin.and_then(|session| session.id().ok()),
+ )
+ .await
+ .to_response()
}
#[derive(Debug, Template)]
@@ -330,14 +583,11 @@ async fn show(
.await
.unwrap_or_default();
let ingredients = entities::prelude::Ingredients::find()
- .filter(
- entities::ingredients::Column::Id.is_in(
- recipe_ingredients
- .iter()
- .map(|i| i.ingredient_id)
- .collect::>(),
- ),
+ .join(
+ JoinType::InnerJoin,
+ entities::ingredients::Relation::RecipeIngredients.def(),
)
+ .filter(entities::recipe_ingredients::Column::RecipeId.eq(recipe.id))
.all(db)
.await
.unwrap_or_default()
@@ -348,6 +598,7 @@ async fn show(
.into_iter()
.filter_map(|ri| {
Some(Ingredient {
+ id: ri.ingredient_id,
name: ingredients.get(&ri.ingredient_id)?.to_owned(),
qty: ri.qty,
unit: ri.unit,
@@ -378,7 +629,7 @@ async fn recipe_form(admin: Identity, db: Data) -> RecipeFor
RecipeForm {
title: "".into(),
summary: "".into(),
- ingeredients: "".into(),
+ ingredients: "".into(),
steps: "".into(),
tags: "".into(),
selected_tags: Default::default(),
@@ -413,6 +664,7 @@ async fn create_recipe(
.map_err(|_| Error::DatabaseError)?;
match save_recipe(form, &mut t).await {
Err(e) => {
+ tracing::warn!("Failed to save changes: {e}");
failure.error = Some(e.to_string());
let _ = t.rollback().await;
failure.known_tags = Tags::find().all(&**db).await.unwrap_or_default();
@@ -470,171 +722,229 @@ async fn save_recipe(form: RecipeForm, t: &mut DatabaseTransaction) -> Result>();
-
- 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)?;
- }
- }
+ create_tags(recipe_id, form.selected_tags, form.tags, &mut *t).await?;
// 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),
+ create_steps(recipe_id, form.steps, &mut *t).await?;
+
+ // Ingredients
+ create_ingredients(recipe_id, form.ingredients, &mut *t).await?;
+
+ Ok(recipe_id)
+}
+
+async fn create_ingredients(
+ recipe_id: i32,
+ ingredients: String,
+ t: &mut DatabaseTransaction,
+) -> Result<(), Error> {
+ use sea_orm::ActiveValue::*;
+
+ let ingredients = crate::utils::parse_ingredients(&ingredients)?;
+ 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::>();
- let _steps = RecipeSteps::insert_many(steps)
- .exec(&mut *t)
- .await
- .inspect_err(|e| tracing::error!("Save steps: {e}"))
- .map_err(|_| Error::SaveRecipeStep)?;
- }
-
- // Ingredients
- {
- let ingredients = crate::utils::parse_ingredients(&form.ingeredients)?;
- let known = Ingredients::find()
- .filter(
- entities::ingredients::Column::Name.is_in(
- ingredients
- .iter()
- .map(|(name, ..)| name.to_string())
- .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)?,
)
- .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()),
+ }
+ v
+ };
+ let map = known
+ .into_iter()
+ .chain(missing.into_iter())
+ .map(|row| (row.name, row.id))
+ .collect::>();
+
+ 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::>();
- 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::>();
+ })
+ .collect::>(),
+ )
+ .exec(&mut *t)
+ .await
+ .inspect_err(|e| tracing::error!("Save ingeredients: {e}"))
+ .map_err(|_| Error::SaveRecipeIngeredient)
+ .map(|_| ())
+}
- 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::>(),
- )
+async fn create_steps(
+ recipe_id: i32,
+ steps: String,
+ t: &mut DatabaseTransaction,
+) -> Result<(), Error> {
+ use sea_orm::ActiveValue::*;
+
+ let steps = crate::utils::parse_steps(&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 ingeredients: {e}"))
- .map_err(|_| Error::SaveRecipeIngeredient)?;
- };
+ .inspect_err(|e| tracing::error!("Save steps: {e}"))
+ .map_err(|_| Error::SaveRecipeStep)?;
- Ok(recipe_id)
+ Ok(())
+}
+
+async fn delete_steps(recipe_id: i32, t: &mut DatabaseTransaction) -> Result<(), Error> {
+ RecipeSteps::delete_many()
+ .filter(entities::recipe_steps::Column::RecipeId.eq(recipe_id))
+ .exec(&mut *t)
+ .await
+ .inspect_err(|e| tracing::error!("Delete steps: {e}"))
+ .map_err(|_| Error::SaveRecipeStep)?;
+ Ok(())
+}
+
+async fn delete_tags(recipe_id: i32, t: &mut DatabaseTransaction) -> Result<(), Error> {
+ RecipeTags::delete_many()
+ .filter(entities::recipe_tags::Column::RecipeId.eq(recipe_id))
+ .exec(&mut *t)
+ .await
+ .inspect_err(|e| tracing::error!("Delete tags: {e}"))
+ .map_err(|_| Error::SaveRecipeTag)?;
+ Ok(())
+}
+
+async fn delete_ingredients(recipe_id: i32, t: &mut DatabaseTransaction) -> Result<(), Error> {
+ RecipeIngredients::delete_many()
+ .filter(entities::recipe_ingredients::Column::RecipeId.eq(recipe_id))
+ .exec(&mut *t)
+ .await
+ .inspect_err(|e| tracing::error!("Delete ingeredients: {e}"))
+ .map_err(|_| Error::SaveRecipeIngeredient)?;
+ Ok(())
+}
+
+async fn create_tags(
+ recipe_id: i32,
+ selected_tags: Option>,
+ tags: String,
+ t: &mut DatabaseTransaction,
+) -> Result<(), Error> {
+ use sea_orm::ActiveValue::*;
+
+ tracing::debug!("Selected tags: {:?}", selected_tags);
+ let selected_tags = selected_tags.unwrap_or_default();
+ let selected_tags = selected_tags
+ .into_iter()
+ .filter_map(|s| s.parse().ok())
+ .collect::>();
+
+ let text_tags = 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)?;
+ }
+ Ok(())
}
#[get("/recipe/{id}/edit")]
@@ -642,7 +952,7 @@ async fn edit_recipe(
id: actix_web::web::Path,
admin: Identity,
db: Data,
-) -> Result {
+) -> Result {
let id = id.into_inner();
let recipe = match Recipies::find()
.filter(entities::recipies::Column::Id.eq(id))
@@ -652,13 +962,17 @@ async fn edit_recipe(
Ok(Some(recipe)) => recipe,
Ok(_) => todo!("index & not found"),
Err(_err) => {
- todo!()
+ return Ok(IndexTemplate::load(&**db, None, admin.id().ok())
+ .await
+ .with_error(_err.to_string())
+ .to_response())
}
};
// steps
let recipe_steps = RecipeSteps::find()
.filter(entities::recipe_steps::Column::RecipeId.eq(id))
+ .order_by_asc(entities::recipe_steps::Column::Id)
.all(&**db)
.await
.inspect_err(|e| tracing::error!("Failed to load recipe steps for edit recipe: {e}"))
@@ -667,6 +981,7 @@ async fn edit_recipe(
// tags
let recipe_tags = RecipeTags::find()
.filter(entities::recipe_tags::Column::RecipeId.eq(id))
+ .order_by_asc(entities::recipe_tags::Column::Id)
.all(&**db)
.await
.inspect_err(|e| tracing::error!("Failed to load recipe tags for edit recipe: {e}"))
@@ -680,6 +995,7 @@ async fn edit_recipe(
// ingredients
let recipe_ingredients = RecipeIngredients::find()
.filter(entities::recipe_ingredients::Column::RecipeId.eq(id))
+ .order_by_asc(entities::recipe_ingredients::Column::Id)
.all(&**db)
.await
.inspect_err(|e| tracing::error!("Failed to load recipe ingredients for edit recipe: {e}"))
@@ -690,6 +1006,7 @@ async fn edit_recipe(
entities::ingredients::Relation::RecipeIngredients.def(),
)
.filter(entities::recipe_ingredients::Column::RecipeId.eq(id))
+ .order_by_asc(entities::ingredients::Column::Name)
.all(&**db)
.await
.inspect_err(|e| tracing::error!("Failed to load ingredients for edit recipe: {e}"))
@@ -705,9 +1022,12 @@ async fn edit_recipe(
});
let steps = recipe_steps
.iter()
- .map(|recipe| match recipe.hint {
- Some(ref hint) => format!("* {body}\n> {hint}", body = recipe.body, hint = hint),
- None => format!("* {body}", body = recipe.body),
+ .map(|recipe| {
+ let body = recipe.body.replace("\n", " ");
+ match recipe.hint {
+ Some(ref hint) => format!("* {body}\n> {hint}", hint = hint),
+ None => format!("* {body}"),
+ }
})
.collect::>()
.join("\n");
@@ -734,27 +1054,33 @@ async fn edit_recipe(
.filter_map(|recipe_tag| Some(tags_by_id.get(&recipe_tag.tag_id)?.name.clone()))
.collect::>()
.join(","),
- selected_tags: recipe_tags
- .iter()
- .map(|rt| rt.recipe_id.to_string())
- .collect(),
- image_url: recipe.image_url,
+ selected_tags: Some(
+ recipe_tags
+ .iter()
+ .map(|rt| rt.recipe_id.to_string())
+ .collect(),
+ ),
time: recipe.time.map(|i| i.to_string()),
author: recipe.author,
error: None,
known_tags: tags.clone(),
- known_ingredients: ingredients.values().cloned().collect(),
+ known_ingredients: ingredients
+ .values()
+ .cloned()
+ .sorted_by(|a, b| a.name.cmp(&b.name))
+ .collect(),
session: admin.id().ok(),
page: Page::Index,
- })
+ }
+ .to_response())
}
-#[post("/recipe/{id}/edit")]
+#[post("/recipe/{id}/update")]
async fn update_recipe(
_id: actix_web::web::Path,
admin: Identity,
db: Data,
- form: QsForm,
+ form: QsForm,
) -> Result {
let mut form = form.into_inner();
form.session = admin.id().ok();
@@ -767,7 +1093,8 @@ async fn update_recipe(
.await
.inspect_err(|e| tracing::error!("Create recipe transaction: {e}"))
.map_err(|_| Error::DatabaseError)?;
- match save_recipe(form, &mut t).await {
+
+ match save_recipe_changes(form, &mut t).await {
Err(e) => {
failure.error = Some(e.to_string());
let _ = t.rollback().await;
@@ -777,15 +1104,6 @@ async fn update_recipe(
}
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())
@@ -793,6 +1111,52 @@ async fn update_recipe(
}
}
+async fn save_recipe_changes(
+ form: EditRecipeForm,
+ t: &mut DatabaseTransaction,
+) -> Result {
+ use crate::entities::recipies::ActiveModel as RAM;
+ use sea_orm::ActiveValue::*;
+
+ let mut summary = String::new();
+ let parser = pulldown_cmark::Parser::new(&form.summary);
+ pulldown_cmark::html::push_html(&mut summary, parser);
+
+ let time = match form.time {
+ Some(s) => Some(
+ humantime::parse_duration(&s)
+ .inspect_err(|e| tracing::warn!("Invalid duration format: {e}"))
+ .map_err(|_| Error::InvalidTime)?
+ .as_secs() as i32,
+ ),
+ None => None,
+ };
+
+ delete_steps(form.id, &mut *t).await?;
+ create_steps(form.id, form.steps, &mut *t).await?;
+
+ delete_tags(form.id, &mut *t).await?;
+ create_tags(form.id, form.selected_tags, form.tags, &mut *t).await?;
+
+ delete_ingredients(form.id, &mut *t).await?;
+ create_ingredients(form.id, form.ingredients, &mut *t).await?;
+
+ Recipies::update(RAM {
+ id: Set(form.id),
+ title: Set(form.title),
+ author: Set(form.author),
+ time: Set(time),
+ summary: Set(Some(summary)),
+ ..Default::default()
+ })
+ .exec(&mut *t)
+ .await
+ .inspect_err(|e| tracing::error!("Failed to save Recipe: {e}"))
+ .map_err(|_| Error::SaveRecipe)?;
+
+ Ok(form.id)
+}
+
#[get("/styles.css")]
async fn styles_css() -> HttpResponse {
HttpResponse::Ok()
@@ -843,5 +1207,9 @@ pub fn configure(config: &mut actix_web::web::ServiceConfig) {
.service(recipe_form)
.service(create_recipe)
.service(recipe_image_upload)
+ .service(by_ingredient)
+ .service(ingredients_list)
+ .service(by_tag)
+ .service(tags_list)
.service(Files::new("/assets", "./assets"));
}
diff --git a/cooked/src/types.rs b/cooked/src/types.rs
index 729c088..e92e4d5 100644
--- a/cooked/src/types.rs
+++ b/cooked/src/types.rs
@@ -7,6 +7,7 @@ pub type User = String;
#[derive(Debug)]
pub struct Ingredient {
+ pub id: i32,
pub qty: i32,
pub name: String,
pub unit: String,
@@ -89,3 +90,38 @@ impl FromStr for Admins {
))
}
}
+
+pub mod ingredient_with_recipe_count {
+ use sea_orm::entity::prelude::*;
+ use serde::{Deserialize, Serialize};
+
+ #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
+ #[sea_orm(table_name = "ingredients")]
+ pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: i32,
+ pub name: String,
+ pub recipe_count: Option,
+ }
+ #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+ pub enum Relation {}
+
+ impl ActiveModelBehavior for ActiveModel {}
+}
+pub mod tag_with_recipe_count {
+ use sea_orm::entity::prelude::*;
+ use serde::{Deserialize, Serialize};
+
+ #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
+ #[sea_orm(table_name = "tags")]
+ pub struct Model {
+ #[sea_orm(primary_key)]
+ pub id: i32,
+ pub name: String,
+ pub recipe_count: Option,
+ }
+ #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+ pub enum Relation {}
+
+ impl ActiveModelBehavior for ActiveModel {}
+}
diff --git a/cooked/src/utils.rs b/cooked/src/utils.rs
index f562df4..47f1a7d 100644
--- a/cooked/src/utils.rs
+++ b/cooked/src/utils.rs
@@ -12,10 +12,16 @@ pub fn parse_steps(s: &str) -> Result)>, Error> {
v.push((line.replacen("*", "", 1).trim().to_string(), None));
}
Some('>') => {
- v.last_mut().ok_or(InvalidStepList)?.1 =
- Some(line.replacen(">", "", 1).trim().to_string());
+ v.last_mut()
+ .ok_or(InvalidStepList)
+ .inspect_err(|_| tracing::warn!("Hint must be part of step."))?
+ .1 = Some(line.replacen(">", "", 1).trim().to_string());
+ }
+ None => return Ok(v),
+ char => {
+ tracing::warn!("Invalid step starting character: {char:?}");
+ return Err(InvalidStepList);
}
- _ => return Err(InvalidStepList),
};
Ok(v)
diff --git a/cooked/templates/ingredient.jinja b/cooked/templates/ingredient.jinja
new file mode 100644
index 0000000..9f31bbe
--- /dev/null
+++ b/cooked/templates/ingredient.jinja
@@ -0,0 +1,28 @@
+{% extends "base.jinja" %}
+
+{% block content %}
+
+ {{ TopBar::new(session)|safe }}
+
+
+
+
+
+ {% for recipe in recipies %}
+ {{ recipe|safe }}
+ {% endfor %}
+
+
+ {% match count %}
+ {% when 0 %}
+ {% when _ %}
+ {% for page in 0..count %}
+
{{page}}
+ {% endfor %}
+ {% endmatch %}
+
+
+
+{% endblock %}
diff --git a/cooked/templates/ingredient_card.jinja b/cooked/templates/ingredient_card.jinja
new file mode 100644
index 0000000..a72faee
--- /dev/null
+++ b/cooked/templates/ingredient_card.jinja
@@ -0,0 +1,12 @@
+
+
+
{{name}}
+
+ {% match recipe_count %}
+ {% when Some with (n) %}
+ {{n}} recipies
+ {% else %}
+ {% endmatch %}
+
+
+
diff --git a/cooked/templates/ingredients.jinja b/cooked/templates/ingredients.jinja
new file mode 100644
index 0000000..b7dfb93
--- /dev/null
+++ b/cooked/templates/ingredients.jinja
@@ -0,0 +1,19 @@
+{% extends "base.jinja" %}
+
+{% block content %}
+
+ {{ TopBar::new(session)|safe }}
+
+
+
+
+
+ {% for ingredient in ingredients %}
+ {{ ingredient|safe }}
+ {% endfor %}
+
+
+
+{% endblock %}
diff --git a/cooked/templates/nav.jinja b/cooked/templates/nav.jinja
index cb7207d..0cc1a5d 100644
--- a/cooked/templates/nav.jinja
+++ b/cooked/templates/nav.jinja
@@ -48,7 +48,7 @@
-
+
Ingeredients
-