Going somewhere.

This commit is contained in:
didier amyot committed 2023-04-03 11:55:34 -04:00
1 parent 0d0337a6aa
commit f140b50be4
17 files changed
+211 -20

No files matched your search

+15 -4
View File
@@ -26,6 +26,15 @@ The rust source must be `/nix/store/pmih7jv9r5xfaif92bza8w05876qvnvc-rust-src/`
nix develop nix develop
``` ```
## Technologies
### Front-end
* htmx
* https://dc-js.github.io/dc.js/
* https://dygraphs.com/
* https://github.com/aftertheflood/sparks
## Todos ## Todos
#### Architecture #### Architecture
@@ -35,10 +44,12 @@ nix develop
#### Backend #### Backend
* [ ] Parametrize `CURRENT_TIMESTAMP` * [ ] Parametrize `CURRENT_TIMESTAMP`
* [ ] Make certain the user that creates the event is allowed to do it.
* [ ] sd
#### Front end #### Front end
* [ ] Serve static files using actix * [x] Serve static files using actix
* [ ] Hack https://github.com/bigskysoftware/htmx/blob/master/src/ext/json-enc.js to all embedded fields. * [x] Hack https://github.com/bigskysoftware/htmx/blob/master/src/ext/json-enc.js to all embedded fields.
* [ ] Create UI to create user * [x] Create UI to create user
* [ ] Create UI to create security * [x] Create UI to create security
+10
View File
@@ -0,0 +1,10 @@
{% set active_navbar_item = 'accounts' %}
{% extends "base.html" %}
{% block body %}
<h1>Account!</h1>
{% endblock %}
+2 -2
View File
@@ -22,9 +22,9 @@
<td><a href="/accounts/{{ account.id }}"><span uk-icon="info"></span></a></td> <td><a href="/accounts/{{ account.id }}"><span uk-icon="info"></span></a></td>
<td>{{ account.id }}</td> <td>{{ account.id }}</td>
<td>{{ account.amount }}</td> <td>{{ account.amount }}</td>
<td>{{ account.account_type }}</td> <td style="font-variant: small-caps;">{{ account.account_type | replace("_", " ") | lower}}</td>
<td>{{ account.last_update }}</td> <td>{{ account.last_update }}</td>
<td>{{ account.security_symbol }}</td> <td style="font-variant: small-caps;">{{ account.security.symbol }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
+2 -1
View File
@@ -16,10 +16,11 @@
hx-get="/partials/actions/action-form" hx-get="/partials/actions/action-form"
hx-trigger="change,load" hx-trigger="change,load"
hx-target="#action-form" hx-target="#action-form"
hx-swap="innerHTML">> hx-swap="innerHTML">
<option value="create_user">Create User</option> <option value="create_user">Create User</option>
<option value="create_security">Create Security</option> <option value="create_security">Create Security</option>
<option value="create_account">Create Account</option> <option value="create_account">Create Account</option>
<option value="emit_securities">Emit Securities</option>
</select> </select>
</div> </div>
<input type="hidden" name="initiator" value="bob le chef"> <input type="hidden" name="initiator" value="bob le chef">
+1 -1
View File
@@ -10,7 +10,7 @@
{'id': 'securities', 'label': 'Securities'}, {'id': 'securities', 'label': 'Securities'},
{'id': 'actions', 'label': 'Actions'}]%} {'id': 'actions', 'label': 'Actions'}]%}
{% for itm in nav_bar_items %} {% for itm in nav_bar_items %}
<li class="{% if active_navbar_item == itm.id %}uk-active"{% endif %}"><a href="{{ itm.id }}">{{ itm.label }}</a></li> <li class="{% if active_navbar_item == itm.id %}uk-active"{% endif %}"><a href="/{{ itm.id }}">{{ itm.label }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@@ -0,0 +1,3 @@
{% for account in accounts %}
<option value="{{ account.id }}">{{ account.id }}</option>
{% endfor %}
@@ -0,0 +1,42 @@
<div class="uk-margin">
<label class="uk-form-label"> Emitter Account
<select class="uk-select"
name="event.EmitSecurities.emitter_account"
id="event.EmitSecurities.emitter_account"
hx-get="/partials/actions/accounts-by-security-selectors"
hx-trigger="change,load"
hx-target="#onetwothree"
hx-vals="js:{'sec-id': document.getElementById('event.EmitSecurities.emitter_account').options[document.getElementById('event.EmitSecurities.emitter_account').selectedIndex].getAttribute('data-sec-id')}"
hx-swap="innerHtml">
{% for account in emitter_accounts %}
<option value="{{ account.id }}" data-sec-id="{{ account.security.id }}">
{{ account.security.name }} ({{ account.security.symbol }})
</option>
{% endfor %}
</select>
</label>
</div>
<div class="uk-margin">
<label class="uk-form-label"> Payee Accounts
<select class="uk-select"
name="event.EmitSecurities.payee_accounts"
id="onetwothree"
multiple>
</select>
</label>
</div>
<div class="uk-margin">
<label class="uk-form-label"> Amount
<input class="uk-input"
type="number"
min="1"
placeholder="100 000" aria-label="Default"
name="event.EmitSecurities.amount"
required>
</label>
</div>
{%include "partials/actions/submit_button.html"%}
+10
View File
@@ -0,0 +1,10 @@
{% set active_navbar_item = 'securities' %}
{% extends "base.html" %}
{% block body %}
<h1>Security!</h1>
{% endblock %}
+25
View File
@@ -195,6 +195,31 @@ pub fn get_account_by_id(tx: &Connection, id: &AccountId) -> CustomResult<Accoun
.map_err(Error::SqlError); .map_err(Error::SqlError);
} }
pub fn get_checking_accounts_for_sec_id(tx: &Connection, sec_id: &SecurityId) -> CustomResult<Vec<Account>> {
let q = "
SELECT acc.id AS acc_id,
acc.amount AS acc_amount,
UNIXEPOCH(acc.timestamp) AS acc_timestamp,
account_type AS acc_account_type,
sec.id AS sec_id,
sec.symbol AS sec_symbol,
sec.security_type AS sec_security_type,
sec.name AS sec_name
FROM accounts acc
JOIN securities sec ON acc.security = sec.id
WHERE sec.id = ?1";
let mut stmt = tx.prepare(q).map_err(Error::SqlError)?;
let ret: Vec<Account> = stmt
.query_map(params![&sec_id], &Account::from_row)
.map_err(Error::SqlError)?
.filter(Result::is_ok)
.map(Result::unwrap)
.collect();
return Ok(ret);
}
fn create_account( fn create_account(
tx: &Connection, tx: &Connection,
sec: &SecurityId, sec: &SecurityId,
+1 -2
View File
@@ -125,7 +125,6 @@ macro_rules! impl_ids {
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub struct AccountId(SafeId); pub struct AccountId(SafeId);
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub struct UserId(SafeId); pub struct UserId(SafeId);
@@ -137,7 +136,7 @@ impl_ids!(UserId, "u");
impl_ids!(SecurityId, "s"); impl_ids!(SecurityId, "s");
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub enum ExternalId { // todo: perhaps this abstraction is useless.. duno enum ExternalId { // todo: perhaps this abstraction is useless.. duno
AccountId(AccountId), AccountId(AccountId),
UserId(UserId), UserId(UserId),
SecurityId(SecurityId), SecurityId(SecurityId),
+1 -1
View File
@@ -29,7 +29,7 @@ pub mod accounts {
pub use super::_accounts::{ pub use super::_accounts::{
add_to_account, create_checking_account, create_security_emitter_account, add_to_account, create_checking_account, create_security_emitter_account,
get_account_by_id, is_owner, list_accounts, Account, AccountChangeReceipt, get_account_by_id, is_owner, list_accounts, Account, AccountChangeReceipt,
list_user_accounts list_user_accounts, AccountType, get_checking_accounts_for_sec_id
}; };
} }
+8 -1
View File
@@ -3,7 +3,7 @@ use crate::db::accounts::{create_checking_account, create_security_emitter_accou
use crate::db::securities::create_security; use crate::db::securities::create_security;
use crate::db::users::create_user; use crate::db::users::create_user;
use crate::events::{CreateNewAccount, CreateNewSecurity, CreateNewUser, Event}; use crate::events::{CreateNewAccount, CreateNewSecurity, CreateNewUser, EmitSecurities, Event};
use crate::util::{CustomResult, Error}; use crate::util::{CustomResult, Error};
fn process_create_new_security_event( fn process_create_new_security_event(
@@ -27,12 +27,19 @@ fn process_create_new_account_event(tx: &mut Transaction, event: CreateNewAccoun
Ok(()) Ok(())
} }
fn process_emit_securities_event(tx: &mut Transaction, event: EmitSecurities) -> CustomResult<()> {
// create_checking_account(&tx, &event.securityId, &event.userId)?;
println!("Here it goes > {:?}", event);
Ok(())
}
pub fn process_event(conn: &mut Connection, event: Event) -> CustomResult<()> { pub fn process_event(conn: &mut Connection, event: Event) -> CustomResult<()> {
let mut tx = conn.transaction().map_err(Error::SqlError)?; let mut tx = conn.transaction().map_err(Error::SqlError)?;
match event { match event {
Event::CreateNewSecurity(e) => process_create_new_security_event(&mut tx, e)?, Event::CreateNewSecurity(e) => process_create_new_security_event(&mut tx, e)?,
Event::CreateNewUser(e) => process_create_new_user_event(&mut tx, e)?, Event::CreateNewUser(e) => process_create_new_user_event(&mut tx, e)?,
Event::CreateNewAccount(e) => process_create_new_account_event(&mut tx, e)?, Event::CreateNewAccount(e) => process_create_new_account_event(&mut tx, e)?,
Event::EmitSecurities(e) => process_emit_securities_event(&mut tx, e)?,
}; };
tx.commit().map_err(Error::SqlError) tx.commit().map_err(Error::SqlError)
} }
+9 -1
View File
@@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::db::external_id::{SecurityId, UserId}; use crate::db::external_id::{AccountId, SecurityId, UserId};
use crate::db::securities::{NewSecurity, SecurityType}; use crate::db::securities::{NewSecurity, SecurityType};
@@ -44,11 +44,19 @@ pub struct CreateNewAccount {
pub userId: UserId, pub userId: UserId,
} }
#[derive(Deserialize, Serialize, Debug)]
pub struct EmitSecurities {
pub amount: i64,
pub emitter_account: AccountId,
pub payee_accounts: Vec<AccountId>,
}
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
pub enum Event { pub enum Event {
CreateNewSecurity(CreateNewSecurity), CreateNewSecurity(CreateNewSecurity),
CreateNewUser(CreateNewUser), CreateNewUser(CreateNewUser),
CreateNewAccount(CreateNewAccount), CreateNewAccount(CreateNewAccount),
EmitSecurities(EmitSecurities),
} }
impl Event { impl Event {
+20 -4
View File
@@ -1,9 +1,12 @@
use rusqlite::Connection; use rusqlite::Connection;
use crate::db::accounts::{Account, list_user_accounts}; use crate::db::accounts::{Account, AccountType, get_checking_accounts_for_sec_id, list_user_accounts};
use crate::db::external_id::{Id, UserId}; use crate::db::external_id::{Id, SecurityId, UserId};
use crate::util::{CustomResult, Error}; use crate::util::{CustomResult, Error};
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use crate::db::securities::Security;
use crate::services::_securities::SecurityDTO;
pub struct AccountService<'a> { pub struct AccountService<'a> {
conn: &'a Connection, conn: &'a Connection,
} }
@@ -14,7 +17,7 @@ pub struct AccountDTO {
pub amount: i64, pub amount: i64,
pub account_type: String, pub account_type: String,
pub last_update: i64, pub last_update: i64,
pub security_symbol: String, pub security: SecurityDTO,
} }
impl AccountDTO { impl AccountDTO {
@@ -24,7 +27,7 @@ impl AccountDTO {
amount: account.amount(), amount: account.amount(),
account_type: account.account_type().to_string(), account_type: account.account_type().to_string(),
last_update: account.timestamp(), last_update: account.timestamp(),
security_symbol: account.security().symbol() security: SecurityDTO::from_entity(account.security())
} }
} }
} }
@@ -43,4 +46,17 @@ impl AccountService<'_> {
Ok(ret.iter().map(AccountDTO::from_entity).collect()) Ok(ret.iter().map(AccountDTO::from_entity).collect())
} }
pub fn get_user_emitter_accounts(& self, user_id: &UserId) -> CustomResult<Vec<AccountDTO>> {
let accounts = self.get_user_accounts(user_id)?;
let ret = accounts.into_iter()
.filter(|acc| acc.account_type == AccountType::SecurityEmitter.to_string())
.collect();
Ok(ret)
}
pub fn get_checking_accounts_for_security(& self, sec_id: & SecurityId) -> CustomResult<Vec<AccountDTO>>{
let ret = get_checking_accounts_for_sec_id(self.conn, sec_id)?;
Ok(ret.iter().map(AccountDTO::from_entity).collect())
}
} }
+2 -2
View File
@@ -3,7 +3,7 @@ use crate::db::securities::{list_securities, Security};
use rusqlite::Connection; use rusqlite::Connection;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Debug)]
pub struct SecurityDTO { pub struct SecurityDTO {
id: SecurityId, id: SecurityId,
symbol: String, symbol: String,
@@ -12,7 +12,7 @@ pub struct SecurityDTO {
} }
impl SecurityDTO { impl SecurityDTO {
fn from_entity(sec: &Security) -> SecurityDTO { pub fn from_entity(sec: &Security) -> SecurityDTO {
SecurityDTO { SecurityDTO {
id: sec.id(), id: sec.id(),
symbol: sec.symbol(), symbol: sec.symbol(),
+3
View File
@@ -67,10 +67,13 @@ fn create_views_scope() -> Scope {
.service(index_views::logout) .service(index_views::logout)
.service(views::actions) .service(views::actions)
.service(views::accounts) .service(views::accounts)
.service(views::account)
.service(views::securities) .service(views::securities)
.service(views::security)
.service(views::overview) .service(views::overview)
.service(views::overview_submit_button) .service(views::overview_submit_button)
.service(views::actions_partials) .service(views::actions_partials)
.service(views::get_accounts_by_security_selectors)
} }
#[tokio::main] #[tokio::main]
+57 -1
View File
@@ -8,6 +8,7 @@ use minijinja::context;
use rusqlite::params; use rusqlite::params;
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
use crate::db::external_id::{Id, SecurityId};
use crate::events::Event; use crate::events::Event;
@@ -32,6 +33,14 @@ pub async fn accounts(tmpl_env: MiniJinjaRenderer,
tmpl_env.render("accounts.html", context!{accounts => accounts}) tmpl_env.render("accounts.html", context!{accounts => accounts})
} }
#[actix_web::get("/accounts/{account_id}")]
pub async fn account(tmpl_env: MiniJinjaRenderer,
req: HttpRequest,
service_factory: web::Data<ServicesFactory>) -> actix_web::Result<impl Responder> {
tmpl_env.render("account.html", context!{})
}
#[actix_web::get("/securities")] #[actix_web::get("/securities")]
@@ -45,6 +54,14 @@ pub async fn securities(tmpl_env: MiniJinjaRenderer,
tmpl_env.render("securities.html", ctx) tmpl_env.render("securities.html", ctx)
} }
#[actix_web::get("/securities/{sec_id}")]
pub async fn security(tmpl_env: MiniJinjaRenderer,
service_factory: web::Data<ServicesFactory>) -> actix_web::Result<impl Responder> {
let ctx = context!{};
tmpl_env.render("security.html", ctx)
}
#[actix_web::get("/overview")] #[actix_web::get("/overview")]
pub async fn overview( pub async fn overview(
tmpl_env: MiniJinjaRenderer, tmpl_env: MiniJinjaRenderer,
@@ -71,6 +88,37 @@ pub async fn overview_submit_button(
tmpl_env.render("partials/overview/submit_button.html", ctx) tmpl_env.render("partials/overview/submit_button.html", ctx)
} }
#[actix_web::get("/partials/actions/accounts-by-security-selectors")]
pub async fn get_accounts_by_security_selectors(
req: HttpRequest,
tmpl_env: MiniJinjaRenderer,
query: web::Query<HashMap<String, String>>,
service_factory: web::Data<ServicesFactory>,
) -> actix_web::Result<Html> {
let sec_id = query
.get("sec-id")
.and_then(SecurityId::from_string)
.ok_or(error::ErrorBadRequest("Invalid sec-id parameters"))?;
let conn = service_factory.get_connection()?;
let account_service = service_factory.get_account_service(&conn)?;
let authentication_service = service_factory.get_authentication_service(&conn)?;
let maybe_user = authentication_service.get_login(&req);
if maybe_user.is_none() {
return Err(error::ErrorForbidden(""))
}
let user_id = maybe_user.unwrap();
let ctx = context! {
sec_id => &sec_id,
user_id => &user_id,
accounts => account_service.get_checking_accounts_for_security(&sec_id).map_err(error::ErrorInternalServerError)?,
};
tmpl_env.render("partials/actions/accounts-by-security-selectors.html", ctx)
}
#[actix_web::get("/partials/actions/action-form")] #[actix_web::get("/partials/actions/action-form")]
pub async fn actions_partials( pub async fn actions_partials(
req: HttpRequest, req: HttpRequest,
@@ -84,11 +132,19 @@ pub async fn actions_partials(
.to_string(); .to_string();
let conn = service_factory.get_connection()?; let conn = service_factory.get_connection()?;
let security_service = service_factory.get_security_service(&conn)?; let security_service = service_factory.get_security_service(&conn)?;
let account_service = service_factory.get_account_service(&conn)?;
let authentication_service = service_factory.get_authentication_service(&conn)?; let authentication_service = service_factory.get_authentication_service(&conn)?;
let maybeUser = authentication_service.get_login(&req);
if maybeUser.is_none() {
return Err(actix_web::error::ErrorForbidden(""))
}
let user = maybeUser.unwrap();
let ctx = context! { let ctx = context! {
event_type => event_type, event_type => event_type,
securities => security_service.list_securities(), securities => security_service.list_securities(),
user => authentication_service.get_logged_in_user(&req).map_err(actix_web::error::ErrorInternalServerError)?, emitter_accounts => account_service.get_user_emitter_accounts(&user).map_err(error::ErrorInternalServerError)?,
accounts => account_service.get_user_emitter_accounts(&user).map_err(error::ErrorInternalServerError)?,
user => authentication_service.get_logged_in_user(&req).map_err(error::ErrorInternalServerError)?,
}; };
let template_location = format!("partials/actions/{:}.html", event_type); let template_location = format!("partials/actions/{:}.html", event_type);
info!("Getting template at: {:?}", template_location); info!("Getting template at: {:?}", template_location);