Add sidenotes

This commit is contained in:
Adrian Woźniak 2024-11-19 11:05:12 +01:00
parent 1171675ee3
commit b84e09ed28
18 changed files with 119 additions and 42 deletions

View File

@ -22,3 +22,6 @@ pub fn is_checked<'s>((id, slice): &(i32, &[String])) -> ::askama::Result<bool>
.filter_map(|s| s.parse::<i32>().ok())
.any(|s| s == *id))
}
pub fn some_or_default<'s>(s: &Option<String>) -> ::askama::Result<String> {
Ok(s.as_ref().cloned().unwrap_or_default())
}

View File

@ -1,4 +1,4 @@
#![feature(iterator_try_collect, duration_constructors)]
#![feature(iterator_try_collect, duration_constructors, string_remove_matches)]
use actix::SyncArbiter;
use actix_files::Files;
@ -7,7 +7,7 @@ use actix_session::{storage::RedisSessionStore, SessionMiddleware};
use actix_web::cookie::Key;
use actix_web::{error, HttpResponse};
use actix_web::{web::Data, App, HttpServer};
use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder};
use actix_web_prom::PrometheusMetricsBuilder;
use sea_orm::Database;
use serde_qs as qs;
use serde_qs::actix::QsQueryConfig;

View File

@ -1,5 +1,6 @@
use crate::actors::search::{Find, Search};
use crate::types::*;
use crate::utils::ParsedIngredient;
use crate::{entities, entities::prelude::*, filters};
use actix_files::Files;
use actix_identity::Identity;
@ -300,7 +301,7 @@ struct ImageUpload {
#[template(path = "recipies/create_form.jinja", ext = "html")]
struct RecipeForm {
title: String,
summary: String,
summary: Option<String>,
ingredients: String,
steps: String,
tags: String,
@ -324,7 +325,7 @@ struct RecipeForm {
struct EditRecipeForm {
id: i32,
title: String,
summary: String,
summary: Option<String>,
ingredients: String,
steps: String,
tags: String,
@ -648,6 +649,7 @@ async fn show(
name: ingredients.get(&ri.ingredient_id)?.to_owned(),
qty: ri.qty,
unit: ri.unit,
sidenote: ri.sidenote,
})
})
.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();
RecipeForm {
title: "".into(),
summary: "".into(),
summary: None,
ingredients: "".into(),
steps: "".into(),
tags: "".into(),
@ -741,7 +743,8 @@ async fn save_recipe(form: RecipeForm, t: &mut DatabaseTransaction) -> Result<i3
use sea_orm::ActiveValue::*;
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);
let time = match form.time {
@ -793,7 +796,7 @@ async fn create_ingredients(
entities::ingredients::Column::Name.is_in(
ingredients
.iter()
.map(|(name, ..)| name.to_string())
.map(|ParsedIngredient { name, .. }| name.to_string())
.collect::<Vec<_>>(),
),
)
@ -808,11 +811,13 @@ async fn create_ingredients(
.collect::<BTreeSet<_>>();
let missing = ingredients
.iter()
.filter(|(name, ..)| !known.contains(name))
.map(|(name, ..)| entities::ingredients::ActiveModel {
.filter(|ParsedIngredient { name, .. }| !known.contains(name))
.map(
|ParsedIngredient { name, .. }| entities::ingredients::ActiveModel {
name: Set(name.to_owned()),
..Default::default()
})
},
)
.collect::<Vec<_>>();
let mut v = Vec::with_capacity(missing.len());
for missing in missing {
@ -835,15 +840,23 @@ async fn create_ingredients(
RecipeIngredients::insert_many(
ingredients
.into_iter()
.filter_map(|(name, unit, qty)| {
.filter_map(
|ParsedIngredient {
name,
unit,
qty,
sidenote,
}| {
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<_>>(),
)
.exec(&mut *t)
@ -1082,7 +1095,7 @@ async fn edit_recipe(
Ok(EditRecipeForm {
id,
title: recipe.title,
summary: recipe.summary.unwrap_or_default(),
summary: recipe.summary,
ingredients: recipe_ingredients
.into_iter()
.filter_map(|ingredient| {
@ -1168,7 +1181,8 @@ async fn save_recipe_changes(
use sea_orm::ActiveValue::*;
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);
let time = match form.time {

View File

@ -11,6 +11,7 @@ pub struct Ingredient {
pub qty: String,
pub name: String,
pub unit: String,
pub sidenote: Option<String>,
}
#[derive(Debug, thiserror::Error)]

View File

@ -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()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.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()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>();
@ -46,12 +66,22 @@ pub fn parse_ingredients(s: &str) -> Result<Vec<(String, Option<String>, String)
// no unit
2 => {
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 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<_>>();
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);
}
}

View File

@ -27,7 +27,7 @@
<label for="summary" class="flex gap-4">
<span class="w-24 shrink-0">Summary:</span>
<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>
</label>

View File

@ -44,6 +44,13 @@
<span class="font-bold">
{{ ingredient.name }}
</span>
{% match ingredient.sidenote %}
{% when Some with (sidenote) %}
<span class="mr-1">
&nbsp;({{ sidenote }})
</span>
{% when None %}
{% endmatch %}
</li>
{% endfor %}
</ul>

View File

@ -34,7 +34,7 @@
<label for="summary" class="flex gap-4">
<span class="w-24 shrink-0">Summary:</span>
<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>
</label>

View File

@ -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 serde::{Deserialize, Serialize};

View File

@ -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;

View File

@ -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::recipe_ingredients::Entity as RecipeIngredients;

View File

@ -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 serde::{Deserialize, Serialize};
@ -12,6 +12,7 @@ pub struct Model {
pub qty: String,
pub unit: String,
pub recipe_id: i32,
pub sidenote: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@ -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 serde::{Deserialize, Serialize};

View File

@ -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 serde::{Deserialize, Serialize};

View File

@ -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 serde::{Deserialize, Serialize};

View File

@ -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 serde::{Deserialize, Serialize};

View File

@ -2,6 +2,7 @@ pub use sea_orm_migration::prelude::*;
mod m20220101_000001_create_table;
mod m20220101_000002_change_quantity_type;
mod m20220101_000003_add_sidenote_to_recipe_ingredient;
pub struct Migrator;
@ -11,6 +12,7 @@ impl MigratorTrait for Migrator {
vec![
Box::new(m20220101_000001_create_table::Migration),
Box::new(m20220101_000002_change_quantity_type::Migration),
Box::new(m20220101_000003_add_sidenote_to_recipe_ingredient::Migration),
]
}
}

View File

@ -33,9 +33,4 @@ impl MigrationTrait for Migration {
#[derive(DeriveIden)]
enum RecipeIngredient {
RecipeIngredients,
Id,
IngredientId,
Qty,
Unit,
RecipeId,
}