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. 218
      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
**/*.rs.bk
.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:
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

@ -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<RenderedField<'a>> {
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<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()
}
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)]
struct ListContext<'a> {
pub struct ListContext<'a> {
pub fields: Vec<RenderedField<'a>>,
pub cards: Vec<RenderedCard<'a>>,
}
#[get("/")]
fn index(store : State<RwLock<Store>>) -> Template {
fn route_index(store: State<RwLock<Store>>) -> 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<RwLock<Store>>) -> Template {
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)]
struct AddCardContext<'a> {
pub fields: Vec<RenderedField<'a>>,
}
#[get("/add")]
fn add(store : State<RwLock<Store>>) -> Template {
#[derive(Serialize)]
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 context = AddCardContext {
@ -88,6 +193,42 @@ fn add(store : State<RwLock<Store>>) -> Template {
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() {
let cwd = env::current_dir().unwrap();
let data_dir = cwd.join("data");
@ -98,7 +239,10 @@ fn main() {
.manage(RwLock::new(store))
.mount("/", StaticFiles::from(cwd.join("templates/static/")))
.mount("/", routes![
index,
add,
route_index,
route_add,
route_add_save,
route_edit,
route_edit_save,
]).launch();
}

@ -1,7 +1,7 @@
use crate::store::model::FieldKind;
use serde_json::Value;
use std::borrow::Cow;
use crate::store::{model, Indexes};
use crate::store::{model, Indexes, Store};
use lazy_static::lazy_static;
@ -155,3 +155,25 @@ impl<'a> RenderedField<'a> {
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 crate::store::model::Model;
use std::collections::HashMap;
use serde::Serialize;
use serde_json::Value;
use indexmap::map::IndexMap;
pub mod model;
pub mod form;
@ -13,7 +16,7 @@ pub mod form;
pub struct Store {
path : PathBuf,
pub model: Model,
pub items : HashMap<usize, serde_json::Value>,
pub data: Cards,
pub index : Indexes,
}
@ -30,6 +33,14 @@ struct RepositoryConfig {
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_DATA_FILE : &'static str = "data.json";
const REPO_INDEX_FILE : &'static str = "index.json";
@ -47,12 +58,12 @@ impl Store {
Store {
path: path.as_ref().into(),
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(),
}
}
pub fn persist(&self) {
pub fn persist(&mut self) {
let mut file = OpenOptions::new()
.write(true)
.create(true)
@ -60,9 +71,37 @@ impl Store {
.open(self.path.join(REPO_DATA_FILE))
.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");
}
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 {

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

@ -2,7 +2,7 @@
{% import "_form_macros" as form %}
{% block title -%}
Add Record
New
{%- endblock %}
{% 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">
<thead>
<tr>
<th>Actions</th>
{%- for field in fields %}
<th>{{ field.label }}</th>
{%- endfor %}
@ -21,6 +22,7 @@
<tbody>
{%- for card in cards %}
<tr>
<td><a href="/edit/{{card.id}}">Edit</a></td>
{%- for field in card.fields %}
<td>
{%- if field.kind == "bool" -%}

Loading…
Cancel
Save