From e76a521356d2a924189de0fdc7fa94564a5579b2 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Sun, 16 Apr 2023 11:34:53 -0400 Subject: [PATCH] Added nice chart for account history. --- README.md | 12 ++- .../migrations/sql/create_tables_001.sql | 3 +- resources/templates/account.html | 70 +++++-------- resources/templates/base.html | 7 +- .../partials/actions/create_security.html | 1 - src/db/_account_history.rs | 99 ------------------- src/db/_accounts.rs | 27 ++++- src/db/mod.rs | 12 +-- src/services/_accounts.rs | 44 ++++++--- src/services/mod.rs | 2 +- src/webui/data_views.rs | 30 ++++++ src/webui/mod.rs | 2 + src/webui/views.rs | 3 - tools/init-dev | 2 + 14 files changed, 126 insertions(+), 188 deletions(-) delete mode 100644 src/db/_account_history.rs create mode 100644 src/webui/data_views.rs create mode 100755 tools/init-dev diff --git a/README.md b/README.md index 38a037c..f12de3d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ nix build ### Building dev environment -#### For ide +#### For IDE ```shell nix build .#development-environment @@ -18,14 +18,20 @@ nix build .#development-environment Then, in IntelliJ use `result/bin` -The rust source must be `/nix/store/pmih7jv9r5xfaif92bza8w05876qvnvc-rust-src/` +The rust source must be `/nix/store/pmih7jv9r5xfaif92bza8w05876qvnvc-rust-src/` or similar. -#### For shell +#### For development shell ```shell nix develop ``` +#### Init Postgres + +```shell + +``` + ## Technologies ### Front-end diff --git a/resources/migrations/sql/create_tables_001.sql b/resources/migrations/sql/create_tables_001.sql index bba12ff..1db0790 100644 --- a/resources/migrations/sql/create_tables_001.sql +++ b/resources/migrations/sql/create_tables_001.sql @@ -80,12 +80,13 @@ CREATE TABLE IF NOT EXISTS accounts CREATE TABLE IF NOT EXISTS accounts_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, account_id INTEGER NOT NULL, account_version INTEGER NOT NULL, amount INTEGER NOT NULL, timestamp DATETIME NOT NULL, FOREIGN KEY (account_id) REFERENCES accounts (id), - PRIMARY KEY (account_id, account_version) + UNIQUE (account_id, account_version) ); CREATE TABLE IF NOT EXISTS account_user_permissions diff --git a/resources/templates/account.html b/resources/templates/account.html index abe7213..db44c09 100644 --- a/resources/templates/account.html +++ b/resources/templates/account.html @@ -15,60 +15,34 @@ Account Type{{ account.account_type | replace("_", " ") | lower}} Last Update{{ account.last_update | utc_date }} Security{{ account.security.symbol }} + Data + data + - +
{% endblock %} \ No newline at end of file diff --git a/resources/templates/base.html b/resources/templates/base.html index 4083a3e..8284c94 100644 --- a/resources/templates/base.html +++ b/resources/templates/base.html @@ -18,9 +18,10 @@ {% if script_chart_js %} - - - + + + + {% endif %} diff --git a/resources/templates/partials/actions/create_security.html b/resources/templates/partials/actions/create_security.html index 86a3b32..459501d 100644 --- a/resources/templates/partials/actions/create_security.html +++ b/resources/templates/partials/actions/create_security.html @@ -29,7 +29,6 @@ - {%include "partials/actions/submit_button.html"%} diff --git a/src/db/_account_history.rs b/src/db/_account_history.rs deleted file mode 100644 index 34ce46e..0000000 --- a/src/db/_account_history.rs +++ /dev/null @@ -1,99 +0,0 @@ -use rusqlite::{Connection, OptionalExtension, params}; -use crate::db::accounts; -use crate::db::accounts::Account; -use crate::db::external_id::AccountId; -use crate::util::{CustomResult, Error}; - -pub struct AccountHistory { - account: Account, - timestamps: Vec, - amounts: Vec, -} - -impl AccountHistory { - pub fn account(&self) -> &Account { - &self.account - } - pub fn timestamps(&self) -> &Vec { - &self.timestamps - } - pub fn amount(&self) -> &Vec { - &self.amounts - } -} - -pub fn get_account_history(tx: &Connection, account_id: &AccountId) -> CustomResult { - let q = " - SELECT amount, UNIXEPOCH(timestamp) AS timestamp - FROM accounts_history - WHERE account_id = ?1 - ORDER BY account_version ASC - "; - - let mut stmt = tx.prepare(q).map_err(Error::SqlError)?; - let rows = stmt - .query_map(params![account_id], |row| Ok((row.get("amount")?, row.get("timestamp")?))) - .optional() - .map_err(Error::SqlError)?; - - let ret: Vec<(i64, i64)> = rows.map(|x| - x.map(Result::unwrap) - .collect()).unwrap_or(vec![]); - - let account = accounts::get_account_by_id(tx, account_id)?; - let mut amounts: Vec = ret.iter().map(|r| r.0).collect(); - let mut timestamps: Vec = ret.iter().map(|r| r.1).collect(); - amounts.push(account.amount()); - timestamps.push(account.timestamp()); - - Ok(AccountHistory{account, timestamps, amounts }) -} - -#[cfg(test)] -mod test_account_history { - use crate::db::test_utils::setup_basic_environment; - use crate::migration; - use crate::util::CustomResult; - use super::*; - - fn setup() -> CustomResult<(Connection, Account)> { - let mut conn = Connection::open_in_memory().unwrap(); - - migration::apply_migrations(&mut conn).unwrap(); - let ret = { - let mut tx = conn.transaction().unwrap(); - let (_, _, accounts) = setup_basic_environment(&mut tx)?; - tx.commit().unwrap(); - accounts.get(0).unwrap().clone() - }; - Ok((conn, ret)) - } - - #[test] - fn test_get_account_history_trivial() -> CustomResult<()>{ - let (mut conn, acc) = setup()?; - let acc_hist = get_account_history(&conn, &acc.id())?; - assert_eq!(acc_hist.amounts, vec![0, 0]); - assert_eq!(acc_hist.timestamps.len(), 2); - assert_eq!(acc_hist.account.label(), "acc1"); - Ok(()) - } - - #[test] - fn test_get_account_history_with_transactions() -> CustomResult<()>{ - let (mut conn, acc) = setup()?; - { - let tx = conn.transaction().unwrap(); - accounts::add_to_account(&tx, &acc.id(), 1)?; - tx.commit().unwrap(); - } - - let tx = conn.transaction().unwrap(); - let acc_hist = get_account_history(&tx, &acc.id())?; - assert_eq!(acc_hist.amounts, vec![0, 1, 1]); - assert_eq!(acc_hist.timestamps.len(), 3); - assert_eq!(acc_hist.account.label(), "acc1"); - Ok(()) - } - -} \ No newline at end of file diff --git a/src/db/_accounts.rs b/src/db/_accounts.rs index 28cbd50..142306d 100644 --- a/src/db/_accounts.rs +++ b/src/db/_accounts.rs @@ -120,7 +120,7 @@ impl Clone for Account { } } #[derive(Debug)] -struct AccountSnapshot { +pub struct AccountSnapshot { amount: i64, version: i64, timestamp: i64, @@ -134,9 +134,18 @@ impl AccountSnapshot { timestamp: row.get("timestamp")?, }) } + pub fn amount(&self) -> i64 { + self.amount + } + pub fn version(&self) -> i64 { + self.version + } + pub fn timestamp(&self) -> i64 { + self.timestamp + } } -struct AccountHistory { +pub struct AccountHistory { id: i64, security: Security, account_type: AccountType, @@ -165,6 +174,15 @@ impl AccountHistory { snapshots, }) } + pub fn security(&self) -> &Security { + &self.security + } + pub fn account_type(&self) -> &AccountType { + &self.account_type + } + pub fn snapshots(&self) -> &Vec { + &self.snapshots + } } pub fn list_accounts<'a>(conn: &Connection) -> CustomResult> { @@ -289,7 +307,7 @@ fn create_account( return get_account_by_id(tx, &id); } -fn get_account_history(tx: &Connection, id: &AccountId) -> CustomResult { +pub fn get_account_history(tx: &Connection, id: &AccountId) -> CustomResult { let q = " SELECT account_version, amount, @@ -361,7 +379,7 @@ impl AccountChangeReceipt { fn update_account_history(tx: &Connection, account_id: &AccountId) -> CustomResult<()> { let q = " - INSERT INTO accounts_history + INSERT INTO accounts_history(account_id, account_version, amount, timestamp) SELECT id AS account_id, version AS account_version, amount, @@ -630,7 +648,6 @@ mod test_accounts { let (mut conn, users, secs) = setup(); let sec = secs.get(0).unwrap().id(); let user = users.get(0).unwrap().id(); - // let sec_emitter_acc = create_security_emitter_account(&conn, &sec, &user, & "sec emitter acc".to_string())?; let checking_account = create_checking_account(&conn, &sec, &user, &"checking acc".to_string())?; match add_to_account(&conn, &checking_account.id(), -33) { diff --git a/src/db/mod.rs b/src/db/mod.rs index dd02678..0c545f9 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -6,7 +6,6 @@ mod _ledger_internal; mod _securities; mod _test_utils; mod _users; -mod _account_history; pub mod ledger_internal { pub use super::_ledger_internal::*; @@ -27,16 +26,7 @@ pub mod users { } pub mod accounts { - pub use super::_accounts::{ - add_to_account, create_checking_account, create_security_emitter_account, - get_account_by_id, is_owner, list_accounts, Account, AccountChangeReceipt, - list_user_accounts, AccountType, get_checking_accounts_for_sec_id, get_number_of_accounts, - get_security_market_cap - }; -} - -pub mod account_history{ - pub use super::_account_history::get_account_history; + pub use super::_accounts::*; } pub mod cheques { diff --git a/src/services/_accounts.rs b/src/services/_accounts.rs index 4ca9070..0358f11 100644 --- a/src/services/_accounts.rs +++ b/src/services/_accounts.rs @@ -1,10 +1,10 @@ use rusqlite::Connection; -use crate::db::accounts::{Account, AccountType, get_checking_accounts_for_sec_id, list_user_accounts}; +use crate::db::accounts::{Account, AccountHistory, AccountType, get_checking_accounts_for_sec_id, list_user_accounts}; use crate::db::external_id::{AccountId, Id, SecurityId, UserId}; use crate::util::{CustomResult, Error}; use serde::Deserialize; use serde::Serialize; -use crate::db::{account_history, accounts}; +use crate::db::{accounts}; use crate::db::securities::Security; use crate::services::_securities::SecurityDTO; @@ -28,9 +28,32 @@ pub struct AccountDTO { } #[derive(Serialize, Debug)] -pub struct AccountHistoryDTO { - amounts: Vec, - timestamps: Vec, +pub struct AccountSnapshotDTO{ + amount: i64, + timestamp: i64, +} + +#[derive(Serialize, Debug)] +pub struct AccountHistoryDTO2{ + id: String, + security: SecurityDTO, + account_type: String, + snapshots: Vec +} + +impl AccountHistoryDTO2 { + fn from_entity(entity: &AccountHistory) -> AccountHistoryDTO2{ + let snapshots = entity.snapshots() + .iter() + .map(|x| AccountSnapshotDTO{amount: x.amount(), timestamp: x.timestamp() * 1000}) + .collect(); + AccountHistoryDTO2{ + id: entity.id().to_string(), + security: SecurityDTO::from_entity(entity.security()), + account_type: entity.account_type().to_string(), + snapshots + } + } } impl AccountDTO { @@ -76,14 +99,9 @@ impl AccountService<'_> { Ok(AccountDTO::from_entity(ret)) } - pub fn get_account_history_by_id(&self, account_id: &AccountId, user_id: &UserId) -> CustomResult { - let ac = account_history::get_account_history(self.conn, account_id)?; - // ret.account() - let ret = AccountHistoryDTO{ - amounts: ac.amount().clone(), - timestamps: ac.timestamps().clone(), - }; - Ok(ret) + pub fn get_account_history_by_id_2(&self, account_id: &AccountId, user_id: &UserId) -> CustomResult { + let ac = accounts::get_account_history(self.conn, account_id)?; + Ok(AccountHistoryDTO2::from_entity(&ac)) } pub fn get_number_of_accounts(&self, sec_id: &SecurityId) -> CustomResult{ diff --git a/src/services/mod.rs b/src/services/mod.rs index c18a760..c1712e9 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,7 +1,7 @@ pub use crate::services::_authentication::SessionService; pub use crate::services::_securities::SecurityService; pub use crate::services::_users::UserService; -pub use crate::services::_accounts::AccountService; +pub use crate::services::_accounts::{AccountService}; pub use crate::services::_transactions::cash_wire_transfer_request; mod _authentication; diff --git a/src/webui/data_views.rs b/src/webui/data_views.rs new file mode 100644 index 0000000..db09982 --- /dev/null +++ b/src/webui/data_views.rs @@ -0,0 +1,30 @@ +use crate::db::external_id::{AccountId, Id}; +use crate::webui::services::ServicesFactory; +use crate::webui::templates::MiniJinjaRenderer; +use actix_web::{error, web, HttpRequest, Responder, Result}; +use serde::Serialize; + + +struct ResponseData{ + +} + +#[actix_web::get("/data/accounts/{account_id}/amount-history.json")] +pub async fn get_account_amount_history( + account_id: web::Path, + req: HttpRequest, + service_factory: web::Data, +) -> Result { + let account_id = AccountId::from_string(&account_id.to_string()).unwrap(); + + let conn = service_factory.get_connection()?; + let auth_service = service_factory.get_authentication_service(&conn)?; + let conn = service_factory.get_connection()?; + let account_service = service_factory.get_account_service(&conn)?; + let user_id = auth_service.get_login(&req).unwrap(); + let ac = account_service + .get_account_history_by_id_2(&account_id, &user_id) + .map_err(error::ErrorInternalServerError)?; + + Ok(web::Json(ac)) +} diff --git a/src/webui/mod.rs b/src/webui/mod.rs index e62ee0d..76be451 100644 --- a/src/webui/mod.rs +++ b/src/webui/mod.rs @@ -28,6 +28,7 @@ mod templates; mod views; mod worker; mod minininja_utils; +mod data_views; struct ProductionEventProcessor; @@ -78,6 +79,7 @@ fn create_views_scope() -> Scope { .service(views::overview_submit_button) .service(views::actions_partials) .service(views::get_accounts_by_security_selectors) + .service(data_views::get_account_amount_history) } #[tokio::main] diff --git a/src/webui/views.rs b/src/webui/views.rs index 45c5f18..29fd41c 100644 --- a/src/webui/views.rs +++ b/src/webui/views.rs @@ -46,13 +46,10 @@ pub async fn get_account(tmpl_env: MiniJinjaRenderer, let ctx = context!{ account => account_service.get_account_by_id(&account_id, &user_id).map_err(error::ErrorInternalServerError)?, - account_history => account_service.get_account_history_by_id(&account_id, &user_id).map_err(error::ErrorInternalServerError)? }; tmpl_env.render("account.html", ctx) } - - #[actix_web::get("/securities")] pub async fn securities(tmpl_env: MiniJinjaRenderer, service_factory: web::Data) -> actix_web::Result { diff --git a/tools/init-dev b/tools/init-dev new file mode 100755 index 0000000..04a09b4 --- /dev/null +++ b/tools/init-dev @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +cargo run users new --name didier.amyot@gmail.com --public-key fooof