diff options
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | migrations/2020-02-04-223649_entry_data/down.sql | 1 | ||||
| -rw-r--r-- | migrations/2020-02-04-223649_entry_data/up.sql | 15 | ||||
| -rw-r--r-- | migrations/2020-02-05-020613_raw_data_entry/down.sql | 5 | ||||
| -rw-r--r-- | migrations/2020-02-05-020613_raw_data_entry/up.sql | 5 | ||||
| -rw-r--r-- | src/db.rs | 62 | ||||
| -rw-r--r-- | src/error.rs | 9 | ||||
| -rw-r--r-- | src/importer.rs | 59 | ||||
| -rw-r--r-- | src/main.rs | 12 | ||||
| -rw-r--r-- | src/models.rs | 15 | ||||
| -rw-r--r-- | src/schema.rs | 22 | ||||
| -rw-r--r-- | src/server.rs | 65 | ||||
| -rw-r--r-- | static/default.css | 58 | ||||
| -rw-r--r-- | templates/index.hbs | 5 | ||||
| -rw-r--r-- | templates/layout.hbs | 21 | ||||
| -rw-r--r-- | templates/profile.hbs | 22 | 
16 files changed, 280 insertions, 98 deletions
| @@ -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; @@ -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)~}} | 
