summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKjetil Orbekk <kjetil.orbekk@gmail.com>2020-02-05 22:26:37 -0500
committerKjetil Orbekk <kjetil.orbekk@gmail.com>2020-02-05 22:26:37 -0500
commitcbe2ff86f1a6873b88e314011ee64b6c25f3359c (patch)
tree03f20093fd59edccb9dcafd1dbb7d1d65fd96c02
parent97b82520aacacbe600c8918b26b5b29b8d47d4d1 (diff)
Extract basics info from strava and display it
-rw-r--r--Cargo.toml2
-rw-r--r--migrations/2020-02-04-223649_entry_data/down.sql1
-rw-r--r--migrations/2020-02-04-223649_entry_data/up.sql15
-rw-r--r--migrations/2020-02-05-020613_raw_data_entry/down.sql5
-rw-r--r--migrations/2020-02-05-020613_raw_data_entry/up.sql5
-rw-r--r--src/db.rs62
-rw-r--r--src/error.rs9
-rw-r--r--src/importer.rs59
-rw-r--r--src/main.rs12
-rw-r--r--src/models.rs15
-rw-r--r--src/schema.rs22
-rw-r--r--src/server.rs65
-rw-r--r--static/default.css58
-rw-r--r--templates/index.hbs5
-rw-r--r--templates/layout.hbs21
-rw-r--r--templates/profile.hbs22
16 files changed, 280 insertions, 98 deletions
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<usize,
Ok(rows)
}
-pub fn insert_entry_data(
+pub fn link_data(
conn: &PgConnection,
- entry_data: &models::EntryData,
+ data: models::RawData,
+ entry_type: &str,
+ entry_id: i64,
) -> Result<usize, Error> {
- 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<Vec<models::RawDataKey>,
.get_results::<models::RawDataKey>(conn)?;
Ok(rows)
}
+
+pub fn get_entries(conn: &PgConnection, username: &str) -> Result<Vec<models::Entry>, Error> {
+ use crate::schema::entries;
+ let r = entries::table
+ .filter(entries::username.eq(username))
+ .get_results::<models::Entry>(conn)?;
+ Ok(r)
+}
+
+// pub fn get_entries_with_data(
+// conn: &PgConnection,
+// username: &str,
+// ) -> Result<Vec<(models::Entry, models::EntryData, models::RawData)>, 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::<Vec<(models::Entry, models::EntryData, models::RawData)>>(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<chrono::format::ParseError> for Error {
+ fn from(e: chrono::format::ParseError) -> Error {
+ Error::ParseTimeError(e)
+ }
+}
+
impl From<StravaApiError> 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<S: strava::StravaApi>(shared: Arc<ImporterSharedData<S>>) {
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<S: strava::StravaApi>(shared: Arc<ImporterSharedData<S>>) {
},
}
- thread::sleep(Duration::from_secs(10));
+ thread::sleep(Duration::from_millis(10));
}
}
@@ -151,8 +151,11 @@ fn to_run(data: &Value) -> Result<Value, Error> {
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<S: strava::StravaApi>(
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<S: strava::StravaApi>(
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<S: strava::StravaApi>(
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<String>,
+ pub entry_id: Option<i64>,
}
#[derive(Insertable, Debug, Serialize, Deserialize, Clone)]
@@ -169,13 +170,3 @@ pub struct Entry {
pub timestamp: Option<DateTime<Utc>>,
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
@@ -18,21 +18,13 @@ 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<Varchar>,
+ entry_id -> Nullable<Int8>,
}
}
@@ -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<String, Json> {
+ 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<LoggedInUser>) -> 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/<username>")]
+fn profile(conn: Db, username: String) -> Result<Template, Error> {
+ 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::<Vec<_>>());
+ context.insert("headings".to_string(), json!(headings));
+ context.insert(
+ "entries".to_string(),
+ json!(entries
+ .into_iter()
+ .map(|e| e.payload)
+ .collect::<Vec<_>>()));
+
+ 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<Template, Error> {
+ let mut context = default_context();
+ context.insert("message".to_string(), json!("Hello, World"));
+ Ok(Template::render("index", context))
}
#[get("/login?<failed>")]
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<Params>) -> 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}}
<p>Message: {{ message }}</p>
<p><a href="/link_strava">Link strava account</a></p>
+{{#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 @@
<!doctype html>
<html lang="en">
<head>
- <title>PJ {{ title }}</title>
+ <meta charset="utf8">
+ <link href="/static/default.css" rel="stylesheet" type="text/css"/>
+ <title>pj- {{ title }}</title>
</head>
<body>
- {{ ~> page }}
+ <header>
+ <h1>{{ title }}</h1>
+ </header>
+ <nav>
+ <ul>
+ <li><a href="">Navigation 1</a></li>
+ <li><a href="">Navigation 2</a></li>
+ <li><a href="">Navigation 3</a></li>
+ </ul>
+ </nav>
+ <main>
+ {{ ~> page }}
+ </main>
+ <footer>
+ <p>Footer goes here</p>
+ </footer>
</body>
</html>
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}}
+ <p>Profile for {{ user }}</p>
+{{/if}}
+<table>
+ <thead>
+ {{#each headings}}
+ <th>{{ this }}</th>
+ {{/each}}
+ </thead>
+ <tbody>
+ {{#each entries}}
+ <tr>
+ {{#each this}}
+ <td>{{ this }}</td>
+ {{/each}}
+ </tr>
+ {{/each}}
+ </tbody>
+</table>
+{{/inline}}
+{{~> (parent)~}}