card editing, keeping order, fixes

session-crate
Ondřej Hruška 5 years ago
parent ed65215723
commit 1f4af57b6e
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 2
      .gitignore
  2. 38
      data/repository.parts.yaml
  3. 37
      data/repository.yaml
  4. 228
      src/main.rs
  5. 24
      src/store/form.rs
  6. 47
      src/store/mod.rs
  7. 6
      templates/_layout.html.tera
  8. 2
      templates/add.html.tera
  9. 22
      templates/edit.html.tera
  10. 2
      templates/index.html.tera

2
.gitignore vendored

@ -1,4 +1,4 @@
/target /target
**/*.rs.bk **/*.rs.bk
.idea/ .idea/
data/data.json

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

@ -1,38 +1,11 @@
model: model:
fields: fields:
category: species:
type: "free_enum" type: "free_enum"
code: breed:
type: "free_enum" type: "free_enum"
value: color:
type: "string" type: "string"
package: alive:
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" type: "bool"
fixed_tags: default: true
type: "tags"
options:
- aaa
- bbb
- ccc
- ddd
- eee

@ -1,9 +1,11 @@
#![feature(proc_macro_hygiene, decl_macro)] #![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] extern crate rocket; #[macro_use]
#[macro_use] extern crate serde; 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::request::FromSegments;
//use rocket::http::uri::Segments; //use rocket::http::uri::Segments;
@ -11,52 +13,32 @@ use rocket_contrib::serve::StaticFiles;
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
mod store; mod store;
use crate::store::Store; use crate::store::Store;
use rocket::State; use rocket::{State, Data};
use parking_lot::RwLock; use parking_lot::RwLock;
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket::http::Status; use rocket::http::Status;
use rocket::request::Form; use rocket::request::{Form, LenientForm, FromForm, FormItems};
use std::collections::HashMap;
use std::env; use std::env;
use crate::store::form::RenderedField; use crate::store::form::{RenderedField, render_empty_fields, RenderedCard, render_card_fields};
use crate::store::model::FieldKind;
fn render_empty_fields<'a>(store : &'a Store) -> Vec<RenderedField<'a>> { use std::ops::Deref;
let indexes = &store.index; use indexmap::map::IndexMap;
store.model.fields.iter().map(|(key, field)| { #[derive(Serialize, Debug)]
RenderedField::from_template_field(key, field, None, indexes) pub struct ListContext<'a> {
}).collect() pub fields: Vec<RenderedField<'a>>,
} pub cards: Vec<RenderedCard<'a>>,
#[derive(Serialize,Debug)]
struct RenderedCard<'a> {
pub fields : Vec<RenderedField<'a>>,
pub id : usize,
}
fn render_card_fields<'a>(store : &'a Store, values : &'a serde_json::Map<String, Value>) -> Vec<RenderedField<'a>> {
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<RenderedField<'a>>,
pub cards : Vec<RenderedCard<'a>>,
} }
#[get("/")] #[get("/")]
fn index(store : State<RwLock<Store>>) -> Template { fn route_index(store: State<RwLock<Store>>) -> Template {
let rg = store.read(); let rg = store.read();
let context = ListContext { let context = ListContext {
fields: render_empty_fields(&rg), 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 { if let Value::Object(map) = card {
Some(RenderedCard { Some(RenderedCard {
fields: render_card_fields(&rg, map), fields: render_card_fields(&rg, map),
@ -71,14 +53,137 @@ fn index(store : State<RwLock<Store>>) -> Template {
Template::render("index", context) Template::render("index", context)
} }
#[derive(Default)]
struct MapFromForm {
pub data: IndexMap<String, String>
}
impl<'a> FromForm<'a> for MapFromForm {
type Error = ();
fn from_form(items: &mut FormItems, _strict: bool) -> Result<Self, Self::Error> {
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::<String, Value> {
let mut card = IndexMap::new();
for (k, field) in &store.model.fields {
let mut value: Option<Value> = 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<String> = 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<String> = 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)] #[derive(Serialize)]
struct AddCardContext<'a> { struct AddCardContext<'a> {
pub fields : Vec<RenderedField<'a>>, pub fields: Vec<RenderedField<'a>>,
} }
#[get("/add")] #[derive(Serialize)]
fn add(store : State<RwLock<Store>>) -> Template { struct EditCardContext<'a> {
pub fields: Vec<RenderedField<'a>>,
pub id : usize,
}
#[get("/add")]
fn route_add(store: State<RwLock<Store>>) -> Template {
let rg = store.read(); let rg = store.read();
let context = AddCardContext { let context = AddCardContext {
@ -88,6 +193,42 @@ fn add(store : State<RwLock<Store>>) -> Template {
Template::render("add", context) Template::render("add", context)
} }
#[post("/add", data = "<form>")]
fn route_add_save(form: Form<MapFromForm>, store: State<RwLock<Store>>) -> 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/<id>")]
fn route_edit(id : usize, store: State<RwLock<Store>>) -> Option<Template> {
let rg = store.read();
if let Some(Value::Object(map)) = rg.data.cards.get(&id) {
let context = EditCardContext {
fields: render_card_fields(&rg, map),
id,
};
Some(Template::render("edit", context))
} else {
None
}
}
#[post("/edit/<id>", data = "<form>")]
fn route_edit_save(id : usize, form: Form<MapFromForm>, store: State<RwLock<Store>>) -> Option<Redirect> {
let mut rg = store.write();
let card = collect_card_form(&rg, form.into_inner());
rg.update_card(id, card);
Some(Redirect::found(uri!(route_index)))
}
fn main() { fn main() {
let cwd = env::current_dir().unwrap(); let cwd = env::current_dir().unwrap();
let data_dir = cwd.join("data"); let data_dir = cwd.join("data");
@ -98,7 +239,10 @@ fn main() {
.manage(RwLock::new(store)) .manage(RwLock::new(store))
.mount("/", StaticFiles::from(cwd.join("templates/static/"))) .mount("/", StaticFiles::from(cwd.join("templates/static/")))
.mount("/", routes![ .mount("/", routes![
index, route_index,
add, route_add,
route_add_save,
route_edit,
route_edit_save,
]).launch(); ]).launch();
} }

@ -1,7 +1,7 @@
use crate::store::model::FieldKind; use crate::store::model::FieldKind;
use serde_json::Value; use serde_json::Value;
use std::borrow::Cow; use std::borrow::Cow;
use crate::store::{model, Indexes}; use crate::store::{model, Indexes, Store};
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -155,3 +155,25 @@ impl<'a> RenderedField<'a> {
rendered rendered
} }
} }
#[derive(Serialize,Debug)]
pub struct RenderedCard<'a> {
pub fields : Vec<RenderedField<'a>>,
pub id : usize,
}
pub fn render_empty_fields(store : &Store) -> Vec<RenderedField> {
let indexes = &store.index;
store.model.fields.iter().map(|(key, field)| {
RenderedField::from_template_field(key, field, None, indexes)
}).collect()
}
pub fn render_card_fields<'a>(store : &'a Store, values : &'a serde_json::Map<String, Value>) -> Vec<RenderedField<'a>> {
let indexes = &store.index;
store.model.fields.iter().map(|(key, field)| {
RenderedField::from_template_field(key, field, values.get(key), indexes)
}).collect()
}

@ -4,6 +4,9 @@ use std::path::{Path, PathBuf};
use rocket::request::FromForm; use rocket::request::FromForm;
use crate::store::model::Model; use crate::store::model::Model;
use std::collections::HashMap; use std::collections::HashMap;
use serde::Serialize;
use serde_json::Value;
use indexmap::map::IndexMap;
pub mod model; pub mod model;
pub mod form; pub mod form;
@ -13,7 +16,7 @@ pub mod form;
pub struct Store { pub struct Store {
path : PathBuf, path : PathBuf,
pub model: Model, pub model: Model,
pub items : HashMap<usize, serde_json::Value>, pub data: Cards,
pub index : Indexes, pub index : Indexes,
} }
@ -30,6 +33,14 @@ struct RepositoryConfig {
pub model : Model, pub model : Model,
} }
#[derive(Serialize,Deserialize,Debug)]
pub struct Cards {
#[serde(default)]
pub cards : IndexMap<usize, Value>,
#[serde(default)]
pub counter: usize,
}
const REPO_CONFIG_FILE : &'static str = "repository.yaml"; const REPO_CONFIG_FILE : &'static str = "repository.yaml";
const REPO_DATA_FILE : &'static str = "data.json"; const REPO_DATA_FILE : &'static str = "data.json";
const REPO_INDEX_FILE : &'static str = "index.json"; const REPO_INDEX_FILE : &'static str = "index.json";
@ -47,12 +58,12 @@ impl Store {
Store { Store {
path: path.as_ref().into(), path: path.as_ref().into(),
model: repository_config.model, model: repository_config.model,
items: serde_json::from_str(&items).expect("Error parsing data file."), data: serde_json::from_str(&items).expect("Error parsing data file."),
index: serde_json::from_str(&indexes).unwrap_or_default(), index: serde_json::from_str(&indexes).unwrap_or_default(),
} }
} }
pub fn persist(&self) { pub fn persist(&mut self) {
let mut file = OpenOptions::new() let mut file = OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
@ -60,9 +71,37 @@ impl Store {
.open(self.path.join(REPO_DATA_FILE)) .open(self.path.join(REPO_DATA_FILE))
.expect("Error opening data file for writing."); .expect("Error opening data file for writing.");
let serialized = serde_json::to_string(&self.items).expect("Error serialize."); let serialized = serde_json::to_string_pretty(&self.data).expect("Error serialize.");
file.write(serialized.as_bytes()).expect("Error write data file"); file.write(serialized.as_bytes()).expect("Error write data file");
} }
pub fn add_card(&mut self, values : IndexMap::<String, Value>) {
let packed = serde_json::to_value(values).expect("Error serialize");
if let p @ Value::Object(_) = packed {
let id = self.data.counter;
self.data.counter += 1;
self.data.cards.insert(id, p);
} else {
panic!("Packing did not produce a map.");
}
self.persist()
}
pub fn update_card(&mut self, id : usize, values : IndexMap::<String, Value>) {
let packed = serde_json::to_value(values).expect("Error serialize");
if !self.data.cards.contains_key(&id) {
panic!("No such card.");
}
if let p @ Value::Object(_) = packed {
self.data.cards.insert(id, p);
} else {
panic!("Packing did not produce a map.");
}
self.persist()
}
} }
fn load_file(path: impl AsRef<Path>) -> String { fn load_file(path: impl AsRef<Path>) -> String {

@ -3,9 +3,9 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{% block title %}{% endblock title %} &bull; Inventory</title> <title>{% block title %}{% endblock title %} &bull; Inventory</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="/style.css">
<link rel="stylesheet" href="taggle.css"> <link rel="stylesheet" href="/taggle.css">
<script src="taggle.min.js"></script> <script src="/taggle.min.js"></script>
</head> </head>
<body> <body>
<nav class="top-nav"> <nav class="top-nav">

@ -2,7 +2,7 @@
{% import "_form_macros" as form %} {% import "_form_macros" as form %}
{% block title -%} {% block title -%}
Add Record New
{%- endblock %} {%- endblock %}
{% block nav -%} {% block nav -%}

@ -0,0 +1,22 @@
{% extends "_layout" %}
{% import "_form_macros" as form %}
{% block title -%}
Edit
{%- endblock %}
{% block nav -%}
<a href="/">Index</a>
{%- endblock %}
{% block content -%}
<form action="/edit/{{id}}" method="POST" class="Form">
<div class="Row indented">
<h1>Edit Record</h1>
</div>
{% include "_fields" %}
<div class="Row indented">
<button type="submit">Save</button>
</div>
</form>
{%- endblock %}

@ -13,6 +13,7 @@
<table class="cards-table"> <table class="cards-table">
<thead> <thead>
<tr> <tr>
<th>Actions</th>
{%- for field in fields %} {%- for field in fields %}
<th>{{ field.label }}</th> <th>{{ field.label }}</th>
{%- endfor %} {%- endfor %}
@ -21,6 +22,7 @@
<tbody> <tbody>
{%- for card in cards %} {%- for card in cards %}
<tr> <tr>
<td><a href="/edit/{{card.id}}">Edit</a></td>
{%- for field in card.fields %} {%- for field in card.fields %}
<td> <td>
{%- if field.kind == "bool" -%} {%- if field.kind == "bool" -%}

Loading…
Cancel
Save