From cbe2ff86f1a6873b88e314011ee64b6c25f3359c Mon Sep 17 00:00:00 2001 From: Kjetil Orbekk Date: Wed, 5 Feb 2020 22:26:37 -0500 Subject: Extract basics info from strava and display it --- Cargo.toml | 2 +- migrations/2020-02-04-223649_entry_data/down.sql | 1 - migrations/2020-02-04-223649_entry_data/up.sql | 15 ----- .../2020-02-05-020613_raw_data_entry/down.sql | 5 ++ migrations/2020-02-05-020613_raw_data_entry/up.sql | 5 ++ src/db.rs | 62 +++++++++++++++++---- src/error.rs | 9 +++ src/importer.rs | 59 ++++++++++++-------- src/main.rs | 12 +++- src/models.rs | 15 +---- src/schema.rs | 22 +------- src/server.rs | 65 +++++++++++++++++----- static/default.css | 58 +++++++++++++++++++ templates/index.hbs | 5 ++ templates/layout.hbs | 21 ++++++- templates/profile.hbs | 22 ++++++++ 16 files changed, 280 insertions(+), 98 deletions(-) delete mode 100644 migrations/2020-02-04-223649_entry_data/down.sql delete mode 100644 migrations/2020-02-04-223649_entry_data/up.sql create mode 100644 migrations/2020-02-05-020613_raw_data_entry/down.sql create mode 100644 migrations/2020-02-05-020613_raw_data_entry/up.sql create mode 100644 static/default.css create mode 100644 templates/profile.hbs diff --git a/Cargo.toml b/Cargo.toml index 0184cb5..27f262a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ serde_json = "1.0" reqwest = { version = "0.10.1", features = ["blocking", "json"] } clap = "2" rocket = "0.4.2" -rocket_contrib = { version = "0.4.2", default-features = false, features = ["handlebars_templates", "diesel_postgres_pool"] } +rocket_contrib = { version = "0.4.2", default-features = false, features = ["handlebars_templates", "diesel_postgres_pool", "serve"] } diesel = { version = "1.0.0", features = ["postgres", "chrono", "extras"] } dotenv = "0.9.0" bcrypt = "0.6" diff --git a/migrations/2020-02-04-223649_entry_data/down.sql b/migrations/2020-02-04-223649_entry_data/down.sql deleted file mode 100644 index a10c419..0000000 --- a/migrations/2020-02-04-223649_entry_data/down.sql +++ /dev/null @@ -1 +0,0 @@ -drop table entry_data; diff --git a/migrations/2020-02-04-223649_entry_data/up.sql b/migrations/2020-02-04-223649_entry_data/up.sql deleted file mode 100644 index dde9288..0000000 --- a/migrations/2020-02-04-223649_entry_data/up.sql +++ /dev/null @@ -1,15 +0,0 @@ -create table entry_data ( - username varchar not null, - entry_type varchar(16) not null, - entry_id bigint not null, - data_type varchar(8) not null, - data_id bigint not null, - -primary key(username, entry_type, entry_id, data_type, data_id), - -foreign key (username, entry_type, entry_id) -references entries(username, entry_type, id), - -foreign key (data_type, data_id) -references raw_data(data_type, id) -); diff --git a/migrations/2020-02-05-020613_raw_data_entry/down.sql b/migrations/2020-02-05-020613_raw_data_entry/down.sql new file mode 100644 index 0000000..342a2d7 --- /dev/null +++ b/migrations/2020-02-05-020613_raw_data_entry/down.sql @@ -0,0 +1,5 @@ +alter table raw_data + drop column if exists entry_type; + +alter table raw_data + drop column if exists entry_id; diff --git a/migrations/2020-02-05-020613_raw_data_entry/up.sql b/migrations/2020-02-05-020613_raw_data_entry/up.sql new file mode 100644 index 0000000..a14d215 --- /dev/null +++ b/migrations/2020-02-05-020613_raw_data_entry/up.sql @@ -0,0 +1,5 @@ +alter table raw_data + add column entry_type varchar(16); + +alter table raw_data + add column entry_id bigint; diff --git a/src/db.rs b/src/db.rs index 5865231..ed1ce4d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,3 +1,4 @@ +use crate::diesel::BoolExpressionMethods; use crate::error::Error; use crate::models; use bcrypt; @@ -6,7 +7,7 @@ use chrono::Utc; use diesel::connection::Connection; use diesel::pg::PgConnection; use diesel::ExpressionMethods; -use diesel::Insertable; +use diesel::JoinOnDsl; use diesel::QueryDsl; use diesel::RunQueryDsl; @@ -44,10 +45,6 @@ macro_rules! insert { i64 ) }; - - (entry_data <= $conn:expr, $values:expr) => { - insert!($conn, schema::entry_data::table, $values) - }; } pub fn create_config(conn: &PgConnection, config: &models::Config) -> Result<(), Error> { @@ -203,13 +200,24 @@ pub fn insert_data(conn: &PgConnection, data: &models::RawData) -> Result Result { - use crate::schema::entry_data; - let rows = diesel::insert_into(entry_data::table) - .values(entry_data) + use crate::schema::raw_data; + let target = raw_data::table.filter( + raw_data::data_type + .eq(data.data_type) + .and(raw_data::id.eq(data.id)), + ); + + let rows = diesel::update(target) + .set(( + raw_data::entry_type.eq(Some(entry_type)), + raw_data::entry_id.eq(Some(entry_id)), + )) .execute(conn)?; Ok(rows) } @@ -232,3 +240,37 @@ pub fn get_raw_data_keys(conn: &PgConnection) -> Result, .get_results::(conn)?; Ok(rows) } + +pub fn get_entries(conn: &PgConnection, username: &str) -> Result, Error> { + use crate::schema::entries; + let r = entries::table + .filter(entries::username.eq(username)) + .get_results::(conn)?; + Ok(r) +} + +// pub fn get_entries_with_data( +// conn: &PgConnection, +// username: &str, +// ) -> Result, Error> { +// use crate::schema::entries; +// use crate::schema::entry_data; +// use crate::schema::raw_data; + +// let r = entries::table +// .filter(entries::username.eq(username)) +// .left_join( +// entry_data::table.on(entries::username.eq(entry_data::username).and( +// entries::entry_type +// .eq(entry_data::entry_type) +// .and(entries::id.eq(entry_data::entry_id)), +// )), +// ) +// .left_join( +// raw_data::table.on(entry_data::data_type +// .eq(raw_data::data_type) +// .and(entry_data::data_id.eq(raw_data::id))), +// ) +// .get_results::>(conn)?; +// Ok(r) +// } diff --git a/src/error.rs b/src/error.rs index d27966a..22ff600 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,5 @@ use bcrypt::BcryptError; +use chrono::format::ParseResult; use diesel::result::Error as DieselErr; use serde_json::Value; use std::convert::From; @@ -80,6 +81,7 @@ pub enum Error { PasswordError(BcryptError), CommunicationError(reqwest::Error), ParseError(serde_json::error::Error), + ParseTimeError(chrono::format::ParseError), StravaApiError(StravaApiError), UnexpectedJson(Value), AlreadyExists, @@ -94,6 +96,7 @@ impl fmt::Display for Error { Error::PasswordError(ref e) => e.fmt(f), Error::CommunicationError(ref e) => e.fmt(f), Error::ParseError(ref e) => e.fmt(f), + Error::ParseTimeError(ref e) => e.fmt(f), Error::UnexpectedJson(_) => f.write_str("UnexpectedJson"), Error::StravaApiError(ref e) => e.fmt(f), Error::AlreadyExists => f.write_str("AlreadyExists"), @@ -103,6 +106,12 @@ impl fmt::Display for Error { } } +impl From for Error { + fn from(e: chrono::format::ParseError) -> Error { + Error::ParseTimeError(e) + } +} + impl From for Error { fn from(e: StravaApiError) -> Error { Error::StravaApiError(e) diff --git a/src/importer.rs b/src/importer.rs index f039d03..7cbc1db 100644 --- a/src/importer.rs +++ b/src/importer.rs @@ -1,10 +1,10 @@ +use chrono::DateTime; use chrono::Utc; use diesel::PgConnection; use serde::Deserialize; use serde::Serialize; use serde_json::to_value; use serde_json::Value; -use std::collections::HashMap; use std::sync::Arc; use std::sync::Mutex; use std::sync::RwLock; @@ -79,7 +79,7 @@ fn handle_tasks(shared: Arc>) { let task = (|| { let conn = shared.conn.lock().unwrap(); let now = Utc::now(); - let eta = now + chrono::Duration::seconds(5); + let eta = now + chrono::Duration::minutes(5); db::take_task(&conn, models::TaskState::NEW, now, eta) })(); @@ -99,7 +99,7 @@ fn handle_tasks(shared: Arc>) { }, } - thread::sleep(Duration::from_secs(10)); + thread::sleep(Duration::from_millis(10)); } } @@ -151,8 +151,11 @@ fn to_run(data: &Value) -> Result { Ok(json!({ "type": "run", - "distance": get!(data f64 "distance")?, - "elapsed_time": get!(data f64 "elapsed_time")?, + "start_timestamp": data["start_date"], + "distance": data["distance"], + "moving_time": data["moving_time"], + "elapsed_time": data["elapsed_time"], + "name": data["name"], })) } @@ -161,33 +164,41 @@ fn process_strava_activity( data: models::RawData, mut task: models::Task, ) -> Result<(), Error> { + if data.entry_type.is_some() && data.entry_id.is_some() { + return Err(Error::InternalError); + } + let json_error = || Error::UnexpectedJson(data.payload.clone()); let strava_type = data.payload["type"].as_str().ok_or_else(json_error)?; - let entry_type = match strava_type { - "Run" => Ok("run".to_string()), + let entry_payload = match strava_type { + "Run" => to_run(&data.payload), &_ => Err(Error::NotFound), }?; + let entry_type = entry_payload["type"] + .as_str() + .ok_or_else(json_error)? + .to_string(); + let timestamp = entry_payload["start_timestamp"] + .as_str() + .map(|t| DateTime::parse_from_rfc3339(t).map(|t| t.with_timezone(&Utc))) + .transpose()?; let conn = &shared.conn.lock().unwrap(); conn.transaction::<(), Error, _>(|| { - let entry = models::NewEntry { - username: data.username.as_str(), - entry_type: entry_type.as_str(), - timestamp: None, - payload: json!({}), + let (entry_type, id) = { + let entry = models::NewEntry { + username: data.username.as_str(), + entry_type: entry_type.as_str(), + timestamp: timestamp, + payload: entry_payload, + }; + info!("Inserting entry: {:#?}", entry); + let id = insert!(entries <= conn, &entry)?; + (entry.entry_type.to_string(), id) }; - let id = insert!(entries <= conn, &entry)?; - - let entry_data = models::EntryData { - username: entry.username.to_string(), - entry_type: entry.entry_type.to_string(), - entry_id: id, - data_type: data.data_type, - data_id: data.id, - }; - insert!(entry_data <= conn, &entry_data)?; + db::link_data(conn, data, &entry_type, id)?; task.state = models::TaskState::SUCCESSFUL; db::update_task(conn, task)?; @@ -203,8 +214,6 @@ fn process_raw_data( mut task: models::Task, ) -> Result<(), Error> { let data = db::get_raw_data(&shared.conn.lock().unwrap(), key)?; - println!("Process raw data: {:#?}", data); - match data.data_type { models::DataType::StravaActivity => process_strava_activity(shared.clone(), data, task)?, }; @@ -278,6 +287,8 @@ fn import_strava_user( id: id, username: username.to_string(), payload: activity.clone(), + entry_type: None, + entry_id: None, }, )?; } diff --git a/src/main.rs b/src/main.rs index 8d6eef8..cf0d308 100644 --- a/src/main.rs +++ b/src/main.rs @@ -87,6 +87,12 @@ fn main() { .takes_value(true) .help("Endpoint for this web server"), ) + .arg( + Arg::with_name("static_path") + .long("static_path") + .takes_value(true) + .help("Directory containing static files"), + ) .subcommand( SubCommand::with_name("init") .about("initialize database config") @@ -129,6 +135,10 @@ fn main() { .value_of("base_url") .unwrap_or("http://localhost:8000"); + let static_path = matches + .value_of("static_path") + .unwrap_or("./static"); + let db_url = matches.value_of("database_url").unwrap(); let conn = PgConnection::establish(db_url).unwrap(); @@ -162,6 +172,6 @@ fn main() { .expect("insert"); } else { info!("Start server"); - pjournal::server::start(conn, db_url, base_url); + pjournal::server::start(conn, db_url, base_url, static_path); } } diff --git a/src/models.rs b/src/models.rs index 15eafd7..83153fd 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,6 +1,5 @@ use crate::schema::config; use crate::schema::entries; -use crate::schema::entry_data; use crate::schema::raw_data; use crate::schema::strava_tokens; use crate::schema::tasks; @@ -143,13 +142,15 @@ pub struct RawDataKey { pub username: String, } -#[derive(Insertable, Queryable, Debug, Serialize, Deserialize, Clone)] +#[derive(Insertable, Queryable, Identifiable, Debug, Serialize, Deserialize, Clone)] #[table_name = "raw_data"] pub struct RawData { pub data_type: DataType, pub id: i64, pub username: String, pub payload: Value, + pub entry_type: Option, + pub entry_id: Option, } #[derive(Insertable, Debug, Serialize, Deserialize, Clone)] @@ -169,13 +170,3 @@ pub struct Entry { pub timestamp: Option>, pub payload: Value, } - -#[derive(Queryable, Insertable, Debug, Serialize, Deserialize, Clone)] -#[table_name = "entry_data"] -pub struct EntryData { - pub username: String, - pub entry_type: String, - pub entry_id: i64, - pub data_type: DataType, - pub data_id: i64, -} diff --git a/src/schema.rs b/src/schema.rs index 23690df..e69bed3 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -17,22 +17,14 @@ table! { } } -table! { - entry_data (username, entry_type, entry_id, data_type, data_id) { - username -> Varchar, - entry_type -> Varchar, - entry_id -> Int8, - data_type -> Varchar, - data_id -> Int8, - } -} - table! { raw_data (data_type, id) { data_type -> Varchar, id -> Int8, username -> Varchar, payload -> Jsonb, + entry_type -> Nullable, + entry_id -> Nullable, } } @@ -67,12 +59,4 @@ joinable!(raw_data -> users (username)); joinable!(strava_tokens -> users (username)); joinable!(tasks -> users (username)); -allow_tables_to_appear_in_same_query!( - config, - entries, - entry_data, - raw_data, - strava_tokens, - tasks, - users, -); +allow_tables_to_appear_in_same_query!(config, entries, raw_data, strava_tokens, tasks, users,); diff --git a/src/server.rs b/src/server.rs index 9c5618b..a203af9 100644 --- a/src/server.rs +++ b/src/server.rs @@ -14,8 +14,11 @@ use rocket::request::Request; use rocket::response::Redirect; use rocket::State; use rocket_contrib::templates::Template; +use serde_json::map::Map; use serde_json::to_value; +use serde_json::Value as Json; use std::collections::HashMap; +use rocket_contrib::serve::StaticFiles; use crate::db; use crate::error::Error; @@ -32,6 +35,12 @@ pub struct LoggedInUser { pub username: String, } +fn default_context() -> Map { + let mut data = Map::new(); + data.insert("parent".to_string(), json!("layout")); + data +} + impl<'a, 'r> FromRequest<'a, 'r> for LoggedInUser { type Error = Error; @@ -63,23 +72,47 @@ impl<'a, 'r> FromRequest<'a, 'r> for LoggedInUser { } } -#[get("/")] -fn index(user: Option) -> Template { - let mut context = HashMap::new(); - context.insert("parent", "layout".to_string()); - context.insert("message", "Hello, World".to_string()); - for user in user { - context.insert("user", user.username); - } - Template::render("index", context) +#[get("/p/")] +fn profile(conn: Db, username: String) -> Result { + let mut context = default_context(); + context.insert("title".to_string(), json!(username)); + + let entries = db::get_entries(&*conn, &username)?; + let headings = + entries.first() + .and_then(|e| e.payload.as_object()) + .map(|e| e.keys().collect::>()); + context.insert("headings".to_string(), json!(headings)); + context.insert( + "entries".to_string(), + json!(entries + .into_iter() + .map(|e| e.payload) + .collect::>())); + + Ok(Template::render("profile", context)) +} + +#[get("/", rank = 1)] +fn index_logged_in(user: LoggedInUser) -> Redirect { + Redirect::to(uri!(profile: user.username)) +} + +#[get("/", rank = 2)] +fn index() -> Result { + let mut context = default_context(); + context.insert("message".to_string(), json!("Hello, World")); + Ok(Template::render("index", context)) } #[get("/login?")] fn login(failed: bool) -> Template { - let mut context = HashMap::new(); - context.insert("parent", "layout"); + let mut context = default_context(); if failed { - context.insert("message", "Incorrect username or password"); + context.insert( + "message".to_string(), + json!("Incorrect username or password"), + ); } Template::render("login", context) } @@ -163,7 +196,8 @@ fn link_strava(params: State) -> Redirect { )) } -pub fn start(conn: diesel::PgConnection, db_url: &str, base_url: &str) { +pub fn start(conn: diesel::PgConnection, db_url: &str, base_url: &str, + static_path: &str) { let mut database_config = HashMap::new(); let mut databases = HashMap::new(); database_config.insert("url", Value::from(db_url)); @@ -196,13 +230,18 @@ pub fn start(conn: diesel::PgConnection, db_url: &str, base_url: &str) { "/", routes![ index, + index_logged_in, login, import_strava, + profile, login_submit, link_strava, link_strava_callback ], ) + .mount( + "/static", + StaticFiles::from(static_path)) .attach(Template::fairing()) .attach(Db::fairing()) .launch(); diff --git a/static/default.css b/static/default.css new file mode 100644 index 0000000..ad13930 --- /dev/null +++ b/static/default.css @@ -0,0 +1,58 @@ +/* Placement */ +body { + display: grid; + margin: 0 auto; + grid-template-columns: auto 200px minmax(800px, 1024px) auto; + grid-template-rows: fit-content 1fr auto; + grid-gap: 0; +} + +header { + grid-column: 3; + grid-row: 1; +} + +nav { + grid-column: 2; + grid-row: 2 / 4; +} + +main { + grid-column: 3; + grid-row: 2; +} + +footer { + grid-column: 2 / 4; + grid-row: 3; +} + +/* Style */ +body { + background-color: #c0dee2; + font-family: sans-serif; +} + +header > h1 { + font-size: 20pt; + text-align: center; +} + +nav ul { + list-style-type: none; + margin: 0; + padding: 0; +} + +nav li a { + display: block; + color: #000; + background-color: #eee; + padding: 8px 16px; + margin: 0 8px 8px 0; + text-decoration: none; +} + +main { + background-color: #eee; +} diff --git a/templates/index.hbs b/templates/index.hbs index 238e5e2..66d89ac 100644 --- a/templates/index.hbs +++ b/templates/index.hbs @@ -4,5 +4,10 @@ {{/if}}

Message: {{ message }}

Link strava account

+{{#each entries}} + {{#each this }} + (thing {{this}}) + {{/each}} +{{/each}} {{/inline}} {{~> (parent)~}} diff --git a/templates/layout.hbs b/templates/layout.hbs index b8de618..90e5e6f 100644 --- a/templates/layout.hbs +++ b/templates/layout.hbs @@ -1,10 +1,27 @@ - PJ {{ title }} + + + pj- {{ title }} - {{ ~> page }} +
+

{{ title }}

+
+ +
+ {{ ~> page }} +
+
+

Footer goes here

+
diff --git a/templates/profile.hbs b/templates/profile.hbs new file mode 100644 index 0000000..240c443 --- /dev/null +++ b/templates/profile.hbs @@ -0,0 +1,22 @@ +{{#*inline "page"}} +{{#if user}} +

Profile for {{ user }}

+{{/if}} + + + {{#each headings}} + + {{/each}} + + + {{#each entries}} + + {{#each this}} + + {{/each}} + + {{/each}} + +
{{ this }}
{{ this }}
+{{/inline}} +{{~> (parent)~}} -- cgit v1.2.3