From 1f4af57b6ead72e994a2d6f448243a1b1ea8d12a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 30 Dec 2019 11:43:39 +0100 Subject: [PATCH] card editing, keeping order, fixes --- .gitignore | 2 +- data/repository.parts.yaml | 38 ++++++ data/repository.yaml | 37 +----- src/main.rs | 228 +++++++++++++++++++++++++++++------- src/store/form.rs | 24 +++- src/store/mod.rs | 47 +++++++- templates/_layout.html.tera | 6 +- templates/add.html.tera | 2 +- templates/edit.html.tera | 22 ++++ templates/index.html.tera | 2 + 10 files changed, 324 insertions(+), 84 deletions(-) create mode 100644 data/repository.parts.yaml create mode 100644 templates/edit.html.tera diff --git a/.gitignore b/.gitignore index 60d1ccc..026b99e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /target **/*.rs.bk .idea/ - +data/data.json diff --git a/data/repository.parts.yaml b/data/repository.parts.yaml new file mode 100644 index 0000000..9091502 --- /dev/null +++ b/data/repository.parts.yaml @@ -0,0 +1,38 @@ +model: + fields: + category: + type: "free_enum" + code: + type: "free_enum" + value: + type: "string" + package: + type: "free_enum" + mounting: + type: "enum" + options: + - "SMD" + - "Through-hole" + - "Screw" + quantity: + type: "int" + min: 0 + location: + type: "free_enum" + sublocation: + label: "Sub-location" + type: "string" + tags: + type: "free_tags" + note: + type: "text" + checkbox: + type: "bool" + fixed_tags: + type: "tags" + options: + - aaa + - bbb + - ccc + - ddd + - eee diff --git a/data/repository.yaml b/data/repository.yaml index 9091502..ea8805a 100644 --- a/data/repository.yaml +++ b/data/repository.yaml @@ -1,38 +1,11 @@ model: fields: - category: + species: type: "free_enum" - code: + breed: type: "free_enum" - value: + color: type: "string" - package: - type: "free_enum" - mounting: - type: "enum" - options: - - "SMD" - - "Through-hole" - - "Screw" - quantity: - type: "int" - min: 0 - location: - type: "free_enum" - sublocation: - label: "Sub-location" - type: "string" - tags: - type: "free_tags" - note: - type: "text" - checkbox: + alive: type: "bool" - fixed_tags: - type: "tags" - options: - - aaa - - bbb - - ccc - - ddd - - eee + default: true diff --git a/src/main.rs b/src/main.rs index ac18fdb..68cf184 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ #![feature(proc_macro_hygiene, decl_macro)] -#[macro_use] extern crate rocket; -#[macro_use] extern crate serde; +#[macro_use] +extern crate rocket; +#[macro_use] +extern crate serde; -use serde_json::{json, Value}; +use serde_json::{json, Value, Number}; //use rocket::request::FromSegments; //use rocket::http::uri::Segments; @@ -11,52 +13,32 @@ use rocket_contrib::serve::StaticFiles; use rocket_contrib::templates::Template; mod store; + use crate::store::Store; -use rocket::State; +use rocket::{State, Data}; use parking_lot::RwLock; use rocket::response::Redirect; use rocket::http::Status; -use rocket::request::Form; -use std::collections::HashMap; +use rocket::request::{Form, LenientForm, FromForm, FormItems}; use std::env; -use crate::store::form::RenderedField; - -fn render_empty_fields<'a>(store : &'a Store) -> Vec> { - let indexes = &store.index; - - store.model.fields.iter().map(|(key, field)| { - RenderedField::from_template_field(key, field, None, indexes) - }).collect() -} - -#[derive(Serialize,Debug)] -struct RenderedCard<'a> { - pub fields : Vec>, - pub id : usize, -} - -fn render_card_fields<'a>(store : &'a Store, values : &'a serde_json::Map) -> Vec> { - let indexes = &store.index; - - store.model.fields.iter().map(|(key, field)| { - RenderedField::from_template_field(key, field, values.get(key), indexes) - }).collect() -} - -#[derive(Serialize,Debug)] -struct ListContext<'a> { - pub fields : Vec>, - pub cards : Vec>, +use crate::store::form::{RenderedField, render_empty_fields, RenderedCard, render_card_fields}; +use crate::store::model::FieldKind; +use std::ops::Deref; +use indexmap::map::IndexMap; + +#[derive(Serialize, Debug)] +pub struct ListContext<'a> { + pub fields: Vec>, + pub cards: Vec>, } #[get("/")] -fn index(store : State>) -> Template { - +fn route_index(store: State>) -> Template { let rg = store.read(); let context = ListContext { fields: render_empty_fields(&rg), - cards: rg.items.iter().filter_map(|(id, card)| { + cards: rg.data.cards.iter().filter_map(|(id, card)| { if let Value::Object(map) = card { Some(RenderedCard { fields: render_card_fields(&rg, map), @@ -71,14 +53,137 @@ fn index(store : State>) -> Template { Template::render("index", context) } +#[derive(Default)] +struct MapFromForm { + pub data: IndexMap +} + +impl<'a> FromForm<'a> for MapFromForm { + type Error = (); + + fn from_form(items: &mut FormItems, _strict: bool) -> Result { + let mut new = MapFromForm::default(); + items.for_each(|item| { + let (k, v) = item.key_value_decoded(); + new.data.insert(k, v); + }); + Ok(new) + } +} + +fn collect_card_form(store : &Store, mut form : MapFromForm) -> IndexMap:: { + let mut card = IndexMap::new(); + + for (k, field) in &store.model.fields { + let mut value: Option = None; + if let Some(input) = form.data.remove(k) { + value = Some(match &field.kind { + FieldKind::Text | FieldKind::String | FieldKind::FreeEnum { .. } => { + Value::String(input) + } + FieldKind::Bool { .. } => { + serde_json::to_value(true).unwrap() + } + FieldKind::Int { min, max, default } => { + let mut val: i64 = if input.is_empty() { + *default + } else { + input.parse().expect("Error parse number") + }; + + if let Some(min) = min { + val = val.max(*min); + } + + if let Some(max) = max { + val = val.min(*max); + } + + serde_json::to_value(val).unwrap() + } + FieldKind::Float { min, max, default } => { + let mut val: f64 = if input.is_empty() { + *default + } else { + input.parse().expect("Error parse number") + }; + + if let Some(min) = min { + val = val.min(*min); + } + + if let Some(max) = max { + val = val.max(*max); + } + + serde_json::to_value(val).unwrap() + } + FieldKind::Enum { options, default } => { + if options.contains(&input) { + Value::String(input) + } else { + let val = default + .as_ref() + .map(ToOwned::to_owned) + .unwrap_or_else(|| options + .first() + .expect("fixed enum must have values") + .to_owned() + ); + + Value::String(val) + } + } + FieldKind::Tags { options } => { + let tags: Vec = input.split(' ') + .map(ToOwned::to_owned) + .filter_map(|tag| { + if options.contains(&tag) { + Some(tag) + } else { + None + } + }).collect(); + + serde_json::to_value(tags).unwrap() + } + FieldKind::FreeTags { .. } => { + let tags: Vec = input.split(' ') + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(ToOwned::to_owned) + .collect(); + + serde_json::to_value(tags).unwrap() + } + }); + } else { + if let FieldKind::Bool { .. } = field.kind { + value = Some(Value::Bool(false)); + } + } + + if let Some(v) = value { + card.insert(k.to_owned(), v); + } + } + + card +} + #[derive(Serialize)] struct AddCardContext<'a> { - pub fields : Vec>, + pub fields: Vec>, } -#[get("/add")] -fn add(store : State>) -> Template { +#[derive(Serialize)] +struct EditCardContext<'a> { + pub fields: Vec>, + pub id : usize, +} +#[get("/add")] +fn route_add(store: State>) -> Template { let rg = store.read(); let context = AddCardContext { @@ -88,6 +193,42 @@ fn add(store : State>) -> Template { Template::render("add", context) } +#[post("/add", data = "
")] +fn route_add_save(form: Form, store: State>) -> Redirect { + let mut rg = store.write(); + + let card = collect_card_form(&rg, form.into_inner()); + rg.add_card(card); + + Redirect::found(uri!(route_index)) +} + +#[get("/edit/")] +fn route_edit(id : usize, store: State>) -> Option