Some refactoring

This commit is contained in:
2020-10-30 23:20:24 +01:00
parent fe5cd04506
commit 24f3d69301
11 changed files with 241 additions and 182 deletions

View File

@ -1,10 +1,9 @@
pub(self) use chrono::NaiveDateTime;
pub(self) use serde_derive::{Serialize, Deserialize};
pub(self) use serde_derive::{Deserialize, Serialize};
pub(self) use uuid::Uuid;
/// Stuff in the default namespace
pub mod global {
}
pub mod global {}
pub mod trees {
pub mod categories {
@ -42,5 +41,3 @@ pub mod trees {
}
}
}

View File

@ -1,13 +1,26 @@
use rocket_contrib::templates::Engines;
use chrono::NaiveDateTime;
use handlebars::handlebars_helper;
use rocket_contrib::templates::Engines;
pub fn register_helpers(engines: &mut Engines) {
handlebars_helper!(pretty_datetime: |dt: str| {
let date = dt.trim_end_matches(|c| c != 'T').trim_end_matches('T');
let time = dt.trim_start_matches(|c| c != 'T').trim_start_matches('T')
.trim_end_matches(|c| c != ':').trim_end_matches(':');
format!("{} {}", date, time)
let dt: NaiveDateTime = dt.parse().unwrap();
format!("{}", dt.format("%Y-%m-%d %H:%M"))
});
engines
.handlebars
.register_helper("pretty_datetime", Box::new(pretty_datetime));
engines.handlebars.register_helper("pretty_datetime", Box::new(pretty_datetime));
handlebars_helper!(pretty_seconds: |secs: u64| {
let minutes = secs / 60;
let hours = minutes / 60;
if hours > 0 {
format!("{}h {}m", hours, minutes)
} else {
format!("{}m", minutes)
}
});
engines
.handlebars
.register_helper("pretty_seconds", Box::new(pretty_seconds));
}

View File

@ -1,13 +1,13 @@
#![feature(decl_macro)]
mod database;
mod handlebars_util;
mod routes;
mod status_json;
mod handlebars_util;
use std::{io, env};
use dotenv::dotenv;
use rocket_contrib::templates::Template;
use rocket_contrib::serve::StaticFiles;
use rocket_contrib::templates::Template;
use std::{env, io};
fn main() -> io::Result<()> {
dotenv().ok();
@ -17,17 +17,19 @@ fn main() -> io::Result<()> {
let sled = sled::open(db_path)?;
let rocket = rocket::ignite()
.attach(Template::custom(|engines| handlebars_util::register_helpers(engines)))
.attach(Template::custom(|engines| {
handlebars_util::register_helpers(engines)
}))
.manage(sled)
.mount("/static", StaticFiles::from("static"))
.mount(
"/",
rocket::routes![
routes::index,
routes::history,
routes::create_category,
routes::activate_category,
routes::deactivate_category,
routes::pages::index,
routes::pages::history,
routes::api::create_category,
routes::api::activate_category,
routes::api::deactivate_category,
],
);
@ -35,4 +37,3 @@ fn main() -> io::Result<()> {
Ok(())
}

View File

@ -1,140 +0,0 @@
use serde_derive::{Serialize, Deserialize};
use rocket::{State, get, post, uri};
use uuid::Uuid;
use rocket_contrib::templates::Template;
use crate::database::v1::trees::{categories, past_sessions};
use crate::status_json::StatusJson;
use rocket::http::Status;
use bincode::{serialize, deserialize};
use rocket::response::Redirect;
use rocket::request::{FromForm, Form};
use sled::Transactional;
use chrono::{Duration, Utc};
use std::collections::HashMap;
#[derive(FromForm)]
pub struct NewCategory {
name: String,
color: String,
}
#[post("/create_category", data="<category>")]
pub fn create_category(category: Form<NewCategory>, db: State<'_, sled::Db>) -> Result<Redirect, StatusJson> {
let category = category.into_inner();
let categories_tree = db.open_tree(categories::NAME)?;
categories_tree.insert(serialize(&Uuid::new_v4())?, serialize(&categories::V {
name: category.name,
color: category.color,
started: None,
})?)?;
Ok(Redirect::to(uri!(index)))
}
#[post("/set_category/<category_uuid>/active")]
pub fn activate_category(category_uuid: String, db: State<'_, sled::Db>) -> Result<StatusJson, StatusJson> {
toggle_category(category_uuid, true, db)
}
#[post("/set_category/<category_uuid>/inactive")]
pub fn deactivate_category(category_uuid: String, db: State<'_, sled::Db>) -> Result<StatusJson, StatusJson> {
toggle_category(category_uuid, false, db)
}
pub fn toggle_category(category_uuid: String, set_active: bool, db: State<'_, sled::Db>) -> Result<StatusJson, StatusJson> {
let category_uuid = Uuid::parse_str(&category_uuid)?;
let category_uuid_s = sled::IVec::from(serialize(&category_uuid)?);
let categories_tree = db.open_tree(categories::NAME)?;
let past_sessions_tree = db.open_tree(past_sessions::NAME)?;
Ok((&categories_tree, &past_sessions_tree)
.transaction(|(tx_categories, tx_past_sessions)| {
match tx_categories.get(&category_uuid_s)? {
None => return Ok(Err(Status::NotFound)),
Some(data) => {
let mut category: categories::V = dbg!(deserialize(&data).unwrap());
let now = Utc::now().naive_utc();
match (set_active, category.started.take()) {
(false, Some(started)) => {
// only save sessions longer than 5 minutes
let duration = now - started;
if duration > Duration::minutes(5) {
let session_uuid = serialize(&Uuid::new_v4()).unwrap();
let past_session = past_sessions::V {
category: category_uuid,
started,
ended: now,
};
tx_past_sessions.insert(session_uuid, serialize(&past_session).unwrap())?;
}
}
(true, None) => {
category.started = Some(now);
}
_ => {
// Category is already in the correct state
return Ok(Ok(Status::Ok.into()));
}
}
tx_categories.insert(&category_uuid_s, serialize(&category).unwrap())?;
}
}
Ok(Ok(Status::Ok.into()))
})??)
}
#[get("/")]
pub fn index(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
#[derive(Debug, Serialize, Deserialize)]
struct TemplateContext {
categories: Vec<(Uuid, categories::V)>,
}
let categories_tree = db.open_tree(categories::NAME)?;
let context = TemplateContext {
categories: categories_tree.iter()
.map(|result| result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v)))))
.collect::<Result<Result<_, _>, _>>()??,
};
Ok(Template::render("index", dbg!(&context)))
}
#[get("/history")]
pub fn history(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
#[derive(Debug, Serialize, Deserialize)]
struct TemplateContext {
sessions: Vec<(categories::V, past_sessions::V)>,
}
let categories_tree = db.open_tree(categories::NAME)?;
let past_sessions_tree = db.open_tree(past_sessions::NAME)?;
let categories: HashMap<categories::K, categories::V> = categories_tree.iter()
.map(|result| result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v)))))
.collect::<Result<Result<_, _>, _>>()??;
let past_sessions: HashMap<past_sessions::K, past_sessions::V> = past_sessions_tree.iter()
.map(|result| result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v)))))
.collect::<Result<Result<_, _>, _>>()??;
let context = TemplateContext {
sessions: past_sessions.into_iter()
.map(|(_, session)| {
let category = categories.get(&session.category).unwrap().clone();
(category, session)
})
.collect(),
};
Ok(Template::render("history", dbg!(&context)))
}

103
src/routes/api.rs Normal file
View File

@ -0,0 +1,103 @@
use crate::database::v1::trees::{categories, past_sessions};
use crate::status_json::StatusJson;
use bincode::{deserialize, serialize};
use chrono::{Duration, Utc};
use rocket::http::Status;
use rocket::request::{Form, FromForm};
use rocket::{post, State};
use sled::Transactional;
use uuid::Uuid;
#[derive(FromForm)]
pub struct NewCategory {
name: String,
color: String,
}
#[post("/create_category", data = "<category>")]
pub fn create_category(
category: Form<NewCategory>,
db: State<'_, sled::Db>,
) -> Result<StatusJson, StatusJson> {
let category = category.into_inner();
let categories_tree = db.open_tree(categories::NAME)?;
categories_tree.insert(
serialize(&Uuid::new_v4())?,
serialize(&categories::V {
name: category.name,
color: category.color,
started: None,
})?,
)?;
Ok(Status::Ok.into())
}
#[post("/set_category/<category_uuid>/active")]
pub fn activate_category(
category_uuid: String,
db: State<'_, sled::Db>,
) -> Result<StatusJson, StatusJson> {
toggle_category(category_uuid, true, db)
}
#[post("/set_category/<category_uuid>/inactive")]
pub fn deactivate_category(
category_uuid: String,
db: State<'_, sled::Db>,
) -> Result<StatusJson, StatusJson> {
toggle_category(category_uuid, false, db)
}
pub fn toggle_category(
category_uuid: String,
set_active: bool,
db: State<'_, sled::Db>,
) -> Result<StatusJson, StatusJson> {
let category_uuid = Uuid::parse_str(&category_uuid)?;
let category_uuid_s = sled::IVec::from(serialize(&category_uuid)?);
let categories_tree = db.open_tree(categories::NAME)?;
let past_sessions_tree = db.open_tree(past_sessions::NAME)?;
Ok((&categories_tree, &past_sessions_tree).transaction(
|(tx_categories, tx_past_sessions)| {
match tx_categories.get(&category_uuid_s)? {
None => return Ok(Err(Status::NotFound)),
Some(data) => {
let mut category: categories::V = deserialize(&data).unwrap();
let now = Utc::now().naive_utc();
match (set_active, category.started.take()) {
(false, Some(started)) => {
// only save sessions longer than 5 minutes
let duration = now - started;
if duration > Duration::minutes(5) {
let session_uuid = serialize(&Uuid::new_v4()).unwrap();
let past_session = past_sessions::V {
category: category_uuid,
started,
ended: now,
};
tx_past_sessions
.insert(session_uuid, serialize(&past_session).unwrap())?;
}
}
(true, None) => {
category.started = Some(now);
}
_ => {
// Category is already in the correct state
return Ok(Ok(Status::Ok.into()));
}
}
tx_categories.insert(&category_uuid_s, serialize(&category).unwrap())?;
}
}
Ok(Ok(Status::Ok.into()))
},
)??)
}

2
src/routes/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod api;
pub mod pages;

82
src/routes/pages.rs Normal file
View File

@ -0,0 +1,82 @@
use crate::database::v1::trees::{categories, past_sessions};
use crate::status_json::StatusJson;
use bincode::deserialize;
use rocket::{get, State};
use rocket_contrib::templates::Template;
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use uuid::Uuid;
#[get("/")]
pub fn index(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
#[derive(Debug, Serialize, Deserialize)]
struct TemplateContext {
categories: Vec<(Uuid, categories::V)>,
}
let categories_tree = db.open_tree(categories::NAME)?;
let context = TemplateContext {
categories: categories_tree
.iter()
.map(|result| {
result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))
})
.collect::<Result<Result<_, _>, _>>()??,
};
Ok(Template::render("index", &context))
}
#[get("/history")]
pub fn history(db: State<'_, sled::Db>) -> Result<Template, StatusJson> {
#[derive(Debug, Serialize, Deserialize)]
struct HistoryEntryContext {
category: categories::V,
session: past_sessions::V,
duration: Duration,
}
#[derive(Debug, Serialize, Deserialize)]
struct TemplateContext {
entries: Vec<HistoryEntryContext>,
}
let categories_tree = db.open_tree(categories::NAME)?;
let past_sessions_tree = db.open_tree(past_sessions::NAME)?;
let categories: HashMap<categories::K, categories::V> = categories_tree
.iter()
.map(|result| {
result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))
})
.collect::<Result<Result<_, _>, _>>()??;
let past_sessions: HashMap<past_sessions::K, past_sessions::V> = past_sessions_tree
.iter()
.map(|result| {
result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))
})
.collect::<Result<Result<_, _>, _>>()??;
let mut context = dbg!(TemplateContext {
entries: past_sessions
.into_iter()
.map(|(_, session)| {
let category = categories.get(&session.category).unwrap().clone();
HistoryEntryContext {
duration: (session.ended - session.started).to_std().unwrap(),
category,
session,
}
})
.collect(),
});
// Newest entries first
context.entries.sort_by_key(|entry| entry.session.started);
context.entries.reverse();
Ok(Template::render("history", &context))
}

View File

@ -59,7 +59,6 @@ impl<'r> Responder<'r> for StatusJson {
}
}
#[duplicate(
status_code T;
[ Status::InternalServerError ] [ sled::Error ];

View File

@ -1,4 +1,4 @@
body {
ody {
font-family: Ubuntu;
}
@ -6,14 +6,14 @@ body {
text-align: center;
}
ul.category_list {
ul.striped_list {
max-width: 40em;
list-style-type: none;
margin: auto;
padding: 0;
}
ul.category_list > li:nth-of-type(odd) {
ul.striped_list > li:nth-of-type(odd) {
background-color: #f0f0f0;
}
@ -80,23 +80,23 @@ ul.category_list > li:nth-of-type(odd) {
border-width: 0px 0 0px 50px;
}
.history_list {
}
.history_entry {
padding: 0.2em;
}
.history_entry_category {
}
.history_entry_started {
.history_entry_duration {
color: #892be1;
}
.history_entry_started {
color: #cc661e;
}
.history_entry_ended {
color: #9f2727;
}

View File

@ -14,16 +14,18 @@
<body>
<h1 class="title">stl</h1>
<div class="history_list">
{{#each sessions}}
<div class="history_entry">
<span class="history_entry_category">{{this.0.name}}</span>
<ul class="striped_list">
{{#each entries}}
<li class="history_entry">
<span class="history_entry_category">{{this.category.name}}</span>
<span> for </span>
<span class="history_entry_duration">{{pretty_seconds this.duration.secs}}</span>
<span> from </span>
<span class="history_entry_started">{{pretty_datetime this.1.started}}</span>
<span class="history_entry_started">{{pretty_datetime this.session.started}}</span>
<span> to </span>
<span class="history_entry_ended">{{pretty_datetime this.1.ended}}</span>
</div>
<span class="history_entry_ended">{{pretty_datetime this.session.ended}}</span>
</li>
{{/each}}
</div>
</ul>
</body>
</html>

View File

@ -46,7 +46,7 @@
<form action="/toggle_category/{{this.0}}" method="post" id="{{this.0}}"></form>
{{/each}}
<ul class="category_list">
<ul class="striped_list">
{{#each categories}}
<li class="category_entry">
<div class="category_icon"