Add sidenotes
This commit is contained in:
parent
1171675ee3
commit
b84e09ed28
@ -22,3 +22,6 @@ pub fn is_checked<'s>((id, slice): &(i32, &[String])) -> ::askama::Result<bool>
|
|||||||
.filter_map(|s| s.parse::<i32>().ok())
|
.filter_map(|s| s.parse::<i32>().ok())
|
||||||
.any(|s| s == *id))
|
.any(|s| s == *id))
|
||||||
}
|
}
|
||||||
|
pub fn some_or_default<'s>(s: &Option<String>) -> ::askama::Result<String> {
|
||||||
|
Ok(s.as_ref().cloned().unwrap_or_default())
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#![feature(iterator_try_collect, duration_constructors)]
|
#![feature(iterator_try_collect, duration_constructors, string_remove_matches)]
|
||||||
|
|
||||||
use actix::SyncArbiter;
|
use actix::SyncArbiter;
|
||||||
use actix_files::Files;
|
use actix_files::Files;
|
||||||
@ -7,7 +7,7 @@ use actix_session::{storage::RedisSessionStore, SessionMiddleware};
|
|||||||
use actix_web::cookie::Key;
|
use actix_web::cookie::Key;
|
||||||
use actix_web::{error, HttpResponse};
|
use actix_web::{error, HttpResponse};
|
||||||
use actix_web::{web::Data, App, HttpServer};
|
use actix_web::{web::Data, App, HttpServer};
|
||||||
use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder};
|
use actix_web_prom::PrometheusMetricsBuilder;
|
||||||
use sea_orm::Database;
|
use sea_orm::Database;
|
||||||
use serde_qs as qs;
|
use serde_qs as qs;
|
||||||
use serde_qs::actix::QsQueryConfig;
|
use serde_qs::actix::QsQueryConfig;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use crate::actors::search::{Find, Search};
|
use crate::actors::search::{Find, Search};
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
|
use crate::utils::ParsedIngredient;
|
||||||
use crate::{entities, entities::prelude::*, filters};
|
use crate::{entities, entities::prelude::*, filters};
|
||||||
use actix_files::Files;
|
use actix_files::Files;
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
@ -300,7 +301,7 @@ struct ImageUpload {
|
|||||||
#[template(path = "recipies/create_form.jinja", ext = "html")]
|
#[template(path = "recipies/create_form.jinja", ext = "html")]
|
||||||
struct RecipeForm {
|
struct RecipeForm {
|
||||||
title: String,
|
title: String,
|
||||||
summary: String,
|
summary: Option<String>,
|
||||||
ingredients: String,
|
ingredients: String,
|
||||||
steps: String,
|
steps: String,
|
||||||
tags: String,
|
tags: String,
|
||||||
@ -324,7 +325,7 @@ struct RecipeForm {
|
|||||||
struct EditRecipeForm {
|
struct EditRecipeForm {
|
||||||
id: i32,
|
id: i32,
|
||||||
title: String,
|
title: String,
|
||||||
summary: String,
|
summary: Option<String>,
|
||||||
ingredients: String,
|
ingredients: String,
|
||||||
steps: String,
|
steps: String,
|
||||||
tags: String,
|
tags: String,
|
||||||
@ -648,6 +649,7 @@ async fn show(
|
|||||||
name: ingredients.get(&ri.ingredient_id)?.to_owned(),
|
name: ingredients.get(&ri.ingredient_id)?.to_owned(),
|
||||||
qty: ri.qty,
|
qty: ri.qty,
|
||||||
unit: ri.unit,
|
unit: ri.unit,
|
||||||
|
sidenote: ri.sidenote,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@ -674,7 +676,7 @@ async fn recipe_form(admin: Identity, db: Data<DatabaseConnection>) -> RecipeFor
|
|||||||
let ingredients = Ingredients::find().all(&**db).await.unwrap_or_default();
|
let ingredients = Ingredients::find().all(&**db).await.unwrap_or_default();
|
||||||
RecipeForm {
|
RecipeForm {
|
||||||
title: "".into(),
|
title: "".into(),
|
||||||
summary: "".into(),
|
summary: None,
|
||||||
ingredients: "".into(),
|
ingredients: "".into(),
|
||||||
steps: "".into(),
|
steps: "".into(),
|
||||||
tags: "".into(),
|
tags: "".into(),
|
||||||
@ -741,7 +743,8 @@ async fn save_recipe(form: RecipeForm, t: &mut DatabaseTransaction) -> Result<i3
|
|||||||
use sea_orm::ActiveValue::*;
|
use sea_orm::ActiveValue::*;
|
||||||
|
|
||||||
let mut summary = String::new();
|
let mut summary = String::new();
|
||||||
let parser = pulldown_cmark::Parser::new(&form.summary);
|
let raw_summary = form.summary.unwrap_or_default();
|
||||||
|
let parser = pulldown_cmark::Parser::new(&raw_summary);
|
||||||
pulldown_cmark::html::push_html(&mut summary, parser);
|
pulldown_cmark::html::push_html(&mut summary, parser);
|
||||||
|
|
||||||
let time = match form.time {
|
let time = match form.time {
|
||||||
@ -793,7 +796,7 @@ async fn create_ingredients(
|
|||||||
entities::ingredients::Column::Name.is_in(
|
entities::ingredients::Column::Name.is_in(
|
||||||
ingredients
|
ingredients
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, ..)| name.to_string())
|
.map(|ParsedIngredient { name, .. }| name.to_string())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -808,11 +811,13 @@ async fn create_ingredients(
|
|||||||
.collect::<BTreeSet<_>>();
|
.collect::<BTreeSet<_>>();
|
||||||
let missing = ingredients
|
let missing = ingredients
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(name, ..)| !known.contains(name))
|
.filter(|ParsedIngredient { name, .. }| !known.contains(name))
|
||||||
.map(|(name, ..)| entities::ingredients::ActiveModel {
|
.map(
|
||||||
name: Set(name.to_owned()),
|
|ParsedIngredient { name, .. }| entities::ingredients::ActiveModel {
|
||||||
..Default::default()
|
name: Set(name.to_owned()),
|
||||||
})
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut v = Vec::with_capacity(missing.len());
|
let mut v = Vec::with_capacity(missing.len());
|
||||||
for missing in missing {
|
for missing in missing {
|
||||||
@ -835,15 +840,23 @@ async fn create_ingredients(
|
|||||||
RecipeIngredients::insert_many(
|
RecipeIngredients::insert_many(
|
||||||
ingredients
|
ingredients
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(name, unit, qty)| {
|
.filter_map(
|
||||||
Some(entities::recipe_ingredients::ActiveModel {
|
|ParsedIngredient {
|
||||||
ingredient_id: Set(*map.get(&name)?),
|
name,
|
||||||
qty: Set(qty),
|
unit,
|
||||||
unit: Set(unit.unwrap_or_default()),
|
qty,
|
||||||
recipe_id: Set(recipe_id),
|
sidenote,
|
||||||
..Default::default()
|
}| {
|
||||||
})
|
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),
|
||||||
|
sidenote: Set(sidenote),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
)
|
)
|
||||||
.exec(&mut *t)
|
.exec(&mut *t)
|
||||||
@ -1082,7 +1095,7 @@ async fn edit_recipe(
|
|||||||
Ok(EditRecipeForm {
|
Ok(EditRecipeForm {
|
||||||
id,
|
id,
|
||||||
title: recipe.title,
|
title: recipe.title,
|
||||||
summary: recipe.summary.unwrap_or_default(),
|
summary: recipe.summary,
|
||||||
ingredients: recipe_ingredients
|
ingredients: recipe_ingredients
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|ingredient| {
|
.filter_map(|ingredient| {
|
||||||
@ -1168,7 +1181,8 @@ async fn save_recipe_changes(
|
|||||||
use sea_orm::ActiveValue::*;
|
use sea_orm::ActiveValue::*;
|
||||||
|
|
||||||
let mut summary = String::new();
|
let mut summary = String::new();
|
||||||
let parser = pulldown_cmark::Parser::new(&form.summary);
|
let raw_summary = form.summary.unwrap_or_default();
|
||||||
|
let parser = pulldown_cmark::Parser::new(&raw_summary);
|
||||||
pulldown_cmark::html::push_html(&mut summary, parser);
|
pulldown_cmark::html::push_html(&mut summary, parser);
|
||||||
|
|
||||||
let time = match form.time {
|
let time = match form.time {
|
||||||
|
@ -11,6 +11,7 @@ pub struct Ingredient {
|
|||||||
pub qty: String,
|
pub qty: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub unit: String,
|
pub unit: String,
|
||||||
|
pub sidenote: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
@ -32,12 +32,32 @@ pub fn parse_steps(s: &str) -> Result<Vec<(String, Option<String>)>, Error> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_ingredients(s: &str) -> Result<Vec<(String, Option<String>, String)>, Error> {
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct ParsedIngredient {
|
||||||
|
pub qty: String,
|
||||||
|
pub name: String,
|
||||||
|
pub unit: Option<String>,
|
||||||
|
pub sidenote: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_ingredients(s: &str) -> Result<Vec<ParsedIngredient>, Error> {
|
||||||
s.lines()
|
s.lines()
|
||||||
.map(|s| s.trim())
|
.map(|s| s.trim())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.map(|s| {
|
.map(|s| {
|
||||||
let mut pieces = s
|
let sidenote = s
|
||||||
|
.chars()
|
||||||
|
.skip_while(|c| *c != '(')
|
||||||
|
.skip_while(|c| *c == '(')
|
||||||
|
.take_while(|c| *c != ')')
|
||||||
|
.collect::<String>()
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let d = s.chars().take_while(|c| *c != '(');
|
||||||
|
let pieces = d.collect::<String>();
|
||||||
|
let mut pieces = pieces
|
||||||
|
.trim()
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@ -46,12 +66,22 @@ pub fn parse_ingredients(s: &str) -> Result<Vec<(String, Option<String>, String)
|
|||||||
// no unit
|
// no unit
|
||||||
2 => {
|
2 => {
|
||||||
let qty = pieces.remove(0);
|
let qty = pieces.remove(0);
|
||||||
Ok((pieces.join(" "), None, qty.to_string()))
|
Ok(ParsedIngredient {
|
||||||
|
name: pieces.join(" "),
|
||||||
|
unit: None,
|
||||||
|
qty: qty.to_string(),
|
||||||
|
sidenote: Some(sidenote).filter(|s| !s.is_empty()),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let qty = pieces.remove(0);
|
let qty = pieces.remove(0);
|
||||||
let unit = pieces.remove(0);
|
let unit = pieces.remove(0);
|
||||||
Ok((pieces.join(" "), Some(unit.to_string()), qty.to_string()))
|
Ok(ParsedIngredient {
|
||||||
|
name: pieces.join(" "),
|
||||||
|
unit: Some(unit.to_string()),
|
||||||
|
qty: qty.to_string(),
|
||||||
|
sidenote: Some(sidenote).filter(|s| !s.is_empty()),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -75,3 +105,27 @@ pub async fn refresh_records(db: Data<DatabaseConnection>, search: Data<Search>)
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let _ = search.send(Refresh { records }).await;
|
let _ = search.send(Refresh { records }).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ingredient_without_unit() {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ingredient_with_unit() {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ingredient_with_summary() {
|
||||||
|
let s = "1 szt pomarańcza (może być czerwona)";
|
||||||
|
let ingredient = parse_ingredients(s).unwrap_or_default();
|
||||||
|
let expected = vec![ParsedIngredient {
|
||||||
|
qty: "1".into(),
|
||||||
|
unit: Some("szt".into()),
|
||||||
|
name: "pomarańcza".into(),
|
||||||
|
sidenote: Some("może być czerwona".into()),
|
||||||
|
}];
|
||||||
|
assert_eq!(ingredient, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
<label for="summary" class="flex gap-4">
|
<label for="summary" class="flex gap-4">
|
||||||
<span class="w-24 shrink-0">Summary:</span>
|
<span class="w-24 shrink-0">Summary:</span>
|
||||||
<div class="flex gap-2 w-full">
|
<div class="flex gap-2 w-full">
|
||||||
<textarea id="summary" name="summary" class="textarea-with-view" required>{{summary}}</textarea>
|
<textarea id="summary" name="summary" class="textarea-with-view" required>{{summary|some_or_default}}</textarea>
|
||||||
<div class="content w-1/2"></div>
|
<div class="content w-1/2"></div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
@ -44,6 +44,13 @@
|
|||||||
<span class="font-bold">
|
<span class="font-bold">
|
||||||
{{ ingredient.name }}
|
{{ ingredient.name }}
|
||||||
</span>
|
</span>
|
||||||
|
{% match ingredient.sidenote %}
|
||||||
|
{% when Some with (sidenote) %}
|
||||||
|
<span class="mr-1">
|
||||||
|
({{ sidenote }})
|
||||||
|
</span>
|
||||||
|
{% when None %}
|
||||||
|
{% endmatch %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
<label for="summary" class="flex gap-4">
|
<label for="summary" class="flex gap-4">
|
||||||
<span class="w-24 shrink-0">Summary:</span>
|
<span class="w-24 shrink-0">Summary:</span>
|
||||||
<div class="flex gap-2 w-full">
|
<div class="flex gap-2 w-full">
|
||||||
<textarea id="summary" name="summary" class="textarea-with-view" required>{{summary}}</textarea>
|
<textarea id="summary" name="summary" class="textarea-with-view" required>{{summary|some_or_default}}</textarea>
|
||||||
<div class="content w-1/2"></div>
|
<div class="content w-1/2"></div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
pub use super::ingredients::Entity as Ingredients;
|
pub use super::ingredients::Entity as Ingredients;
|
||||||
pub use super::recipe_ingredients::Entity as RecipeIngredients;
|
pub use super::recipe_ingredients::Entity as RecipeIngredients;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -12,6 +12,7 @@ pub struct Model {
|
|||||||
pub qty: String,
|
pub qty: String,
|
||||||
pub unit: String,
|
pub unit: String,
|
||||||
pub recipe_id: i32,
|
pub recipe_id: i32,
|
||||||
|
pub sidenote: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.11
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -2,6 +2,7 @@ pub use sea_orm_migration::prelude::*;
|
|||||||
|
|
||||||
mod m20220101_000001_create_table;
|
mod m20220101_000001_create_table;
|
||||||
mod m20220101_000002_change_quantity_type;
|
mod m20220101_000002_change_quantity_type;
|
||||||
|
mod m20220101_000003_add_sidenote_to_recipe_ingredient;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ impl MigratorTrait for Migrator {
|
|||||||
vec![
|
vec![
|
||||||
Box::new(m20220101_000001_create_table::Migration),
|
Box::new(m20220101_000001_create_table::Migration),
|
||||||
Box::new(m20220101_000002_change_quantity_type::Migration),
|
Box::new(m20220101_000002_change_quantity_type::Migration),
|
||||||
|
Box::new(m20220101_000003_add_sidenote_to_recipe_ingredient::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,9 +33,4 @@ impl MigrationTrait for Migration {
|
|||||||
#[derive(DeriveIden)]
|
#[derive(DeriveIden)]
|
||||||
enum RecipeIngredient {
|
enum RecipeIngredient {
|
||||||
RecipeIngredients,
|
RecipeIngredients,
|
||||||
Id,
|
|
||||||
IngredientId,
|
|
||||||
Qty,
|
|
||||||
Unit,
|
|
||||||
RecipeId,
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user