From 4e73e8b508a2b7ab317ad43549953170f8dcc1a6 Mon Sep 17 00:00:00 2001 From: Aaron Lindsay Date: Wed, 21 Jun 2017 21:25:38 -0400 Subject: [PATCH] Add per-user default currency --- js/actions/SecurityTemplateActions.js | 48 +++++++++- js/components/AccountSettingsModal.js | 26 ++++++ js/components/MoneyGoApp.js | 1 + js/components/NewUserModal.js | 26 ++++++ js/constants/SecurityTemplateConstants.js | 2 + .../AccountSettingsModalContainer.js | 4 +- js/containers/MoneyGoAppContainer.js | 2 + js/containers/NewUserModalContainer.js | 4 +- js/models.js | 4 + js/reducers/SecurityTemplateReducer.js | 29 +++--- securities.go | 21 ++++- users.go | 93 +++++++++++++++++-- 12 files changed, 237 insertions(+), 23 deletions(-) diff --git a/js/actions/SecurityTemplateActions.js b/js/actions/SecurityTemplateActions.js index a92ae37..15d9513 100644 --- a/js/actions/SecurityTemplateActions.js +++ b/js/actions/SecurityTemplateActions.js @@ -5,6 +5,7 @@ var ErrorActions = require('./ErrorActions'); var models = require('../models.js'); var Security = models.Security; var Error = models.Error; +var SecurityType = models.SecurityType; function searchSecurityTemplates(searchString, searchType) { return { @@ -23,6 +24,19 @@ function securityTemplatesSearched(searchString, searchType, securities) { } } +function fetchCurrencyTemplates() { + return { + type: SecurityTemplateConstants.FETCH_CURRENCIES + } +} + +function currencyTemplatesFetched(currencies) { + return { + type: SecurityTemplateConstants.CURRENCIES_FETCHED, + currencies: currencies + } +} + function search(searchString, searchType, limit) { return function (dispatch) { dispatch(searchSecurityTemplates(searchString, searchType)); @@ -57,6 +71,38 @@ function search(searchString, searchType, limit) { }; } +function fetchCurrencies() { + return function (dispatch) { + dispatch(fetchCurrencyTemplates()); + + $.ajax({ + type: "GET", + dataType: "json", + url: "securitytemplate/?search=&type=currency", + success: function(data, status, jqXHR) { + var e = new Error(); + e.fromJSON(data); + if (e.isError()) { + dispatch(ErrorActions.serverError(e)); + } else if (data.securities == null) { + dispatch(currencyTemplatesFetched(new Array())); + } else { + dispatch(currencyTemplatesFetched( + data.securities.map(function(json) { + var s = new Security(); + s.fromJSON(json); + return s; + }))); + } + }, + error: function(jqXHR, status, error) { + dispatch(ErrorActions.ajaxError(error)); + } + }); + }; +} + module.exports = { - search: search + search: search, + fetchCurrencies: fetchCurrencies }; diff --git a/js/components/AccountSettingsModal.js b/js/components/AccountSettingsModal.js index 00eb757..ccee8a2 100644 --- a/js/components/AccountSettingsModal.js +++ b/js/components/AccountSettingsModal.js @@ -12,6 +12,8 @@ var FormControl = ReactBootstrap.FormControl; var ControlLabel = ReactBootstrap.ControlLabel; var Col = ReactBootstrap.Col; +var Combobox = require('react-widgets').Combobox; + var models = require('../models'); var User = models.User; @@ -22,6 +24,7 @@ class AccountSettingsModal extends React.Component { name: props ? props.user.Name: "", username: props ? props.user.Username : "", email: props ? props.user.Email : "", + defaultCurrency: props ? props.user.DefaultCurrency : "", password: models.BogusPassword, confirm_password: models.BogusPassword, passwordChanged: false, @@ -33,6 +36,7 @@ class AccountSettingsModal extends React.Component { this.state = this._getInitialState(); this.onCancel = this.handleCancel.bind(this); this.onChange = this.handleChange.bind(this); + this.onSelectCurrency = this.handleSelectCurrency.bind(this); this.onSubmit = this.handleSubmit.bind(this); } componentWillReceiveProps(nextProps) { @@ -73,6 +77,13 @@ class AccountSettingsModal extends React.Component { confirm_password: ReactDOM.findDOMNode(this.refs.confirm_password).value }); } + handleSelectCurrency(security) { + if (security.hasOwnProperty('SecurityId')) { + this.setState({ + defaultCurrency: security.SecurityId + }); + } + } handleSubmit(e) { var u = new User(); e.preventDefault(); @@ -81,6 +92,7 @@ class AccountSettingsModal extends React.Component { u.Name = this.state.name; u.Username = this.state.username; u.Email = this.state.email; + u.DefaultCurrency = this.state.defaultCurrency; if (this.state.passwordChanged) { u.Password = this.state.password; if (u.Password != this.state.confirm_password) { @@ -130,6 +142,20 @@ class AccountSettingsModal extends React.Component { ref="email"/> + + Default Currency + + item == undefined || typeof item === 'string' ? item : item.Name + " - " + item.Description} + defaultValue={this.state.defaultCurrency} + onChange={this.onSelectCurrency} + suggest + filter='contains' + ref="security" /> + + Password diff --git a/js/components/MoneyGoApp.js b/js/components/MoneyGoApp.js index d4b4c4b..b8046b2 100644 --- a/js/components/MoneyGoApp.js +++ b/js/components/MoneyGoApp.js @@ -27,6 +27,7 @@ class MoneyGoApp extends React.Component { } componentDidMount() { this.props.tryResumingSession(); + this.props.fetchCurrencies(); } handleShowSettings() { this.setState({showAccountSettingsModal: true}); diff --git a/js/components/NewUserModal.js b/js/components/NewUserModal.js index dc8587b..b29b6d0 100644 --- a/js/components/NewUserModal.js +++ b/js/components/NewUserModal.js @@ -11,6 +11,8 @@ var Col = ReactBootstrap.Col; var Button = ReactBootstrap.Button; var ButtonGroup = ReactBootstrap.ButtonGroup; +var Combobox = require('react-widgets').Combobox; + var models = require('../models'); var User = models.User; @@ -22,6 +24,7 @@ class NewUserModal extends React.Component { name: "", username: "", email: "", + defaultCurrency: '840', // ISO4217 code for USD password: "", confirm_password: "", passwordChanged: false, @@ -29,6 +32,7 @@ class NewUserModal extends React.Component { }; this.onCancel = this.handleCancel.bind(this); this.onChange = this.handleChange.bind(this); + this.onSelectCurrency = this.handleSelectCurrency.bind(this); this.onSubmit = this.handleSubmit.bind(this); } passwordValidationState() { @@ -64,6 +68,13 @@ class NewUserModal extends React.Component { confirm_password: ReactDOM.findDOMNode(this.refs.confirm_password).value }); } + handleSelectCurrency(security) { + if (security.hasOwnProperty('SecurityId')) { + this.setState({ + defaultCurrency: security.AlternateId + }); + } + } handleSubmit(e) { var u = new User(); var error = ""; @@ -72,6 +83,7 @@ class NewUserModal extends React.Component { u.Name = this.state.name; u.Username = this.state.username; u.Email = this.state.email; + u.DefaultCurrency = Number.parseInt(this.state.defaultCurrency); u.Password = this.state.password; if (u.Password != this.state.confirm_password) { this.setState({error: "Error: passwords do not match"}); @@ -118,6 +130,20 @@ class NewUserModal extends React.Component { ref="email"/> + + Default Currency + + typeof item === 'string' ? item : item.Name + " - " + item.Description} + defaultValue={this.state.defaultCurrency} + onChange={this.onSelectCurrency} + suggest + filter='contains' + ref="security" /> + + Password diff --git a/js/constants/SecurityTemplateConstants.js b/js/constants/SecurityTemplateConstants.js index b8fc901..d0dd5b5 100644 --- a/js/constants/SecurityTemplateConstants.js +++ b/js/constants/SecurityTemplateConstants.js @@ -1,6 +1,8 @@ var keyMirror = require('keymirror'); module.exports = keyMirror({ + FETCH_CURRENCIES: null, + CURRENCIES_FETCHED: null, SEARCH_SECURITY_TEMPLATES: null, SECURITY_TEMPLATES_SEARCHED: null }); diff --git a/js/containers/AccountSettingsModalContainer.js b/js/containers/AccountSettingsModalContainer.js index f653083..ade3aba 100644 --- a/js/containers/AccountSettingsModalContainer.js +++ b/js/containers/AccountSettingsModalContainer.js @@ -1,11 +1,13 @@ var connect = require('react-redux').connect; var UserActions = require('../actions/UserActions'); + var AccountSettingsModal = require('../components/AccountSettingsModal'); function mapStateToProps(state) { return { - user: state.user + user: state.user, + currencies: state.securities.currency_list } } diff --git a/js/containers/MoneyGoAppContainer.js b/js/containers/MoneyGoAppContainer.js index b348c6e..8a9edf8 100644 --- a/js/containers/MoneyGoAppContainer.js +++ b/js/containers/MoneyGoAppContainer.js @@ -1,6 +1,7 @@ var connect = require('react-redux').connect; var UserActions = require('../actions/UserActions'); +var SecurityTemplateActions = require('../actions/SecurityTemplateActions'); var MoneyGoApp = require('../components/MoneyGoApp'); @@ -13,6 +14,7 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { tryResumingSession: function() {dispatch(UserActions.tryResumingSession())}, + fetchCurrencies: function() {dispatch(SecurityTemplateActions.fetchCurrencies())}, } } diff --git a/js/containers/NewUserModalContainer.js b/js/containers/NewUserModalContainer.js index 4b7f8d9..237843b 100644 --- a/js/containers/NewUserModalContainer.js +++ b/js/containers/NewUserModalContainer.js @@ -5,7 +5,9 @@ var UserActions = require('../actions/UserActions'); var NewUserModal = require('../components/NewUserModal'); function mapStateToProps(state) { - return {} + return { + currencies: state.securityTemplates.currencies + } } function mapDispatchToProps(dispatch) { diff --git a/js/models.js b/js/models.js index 2890840..b3df031 100644 --- a/js/models.js +++ b/js/models.js @@ -13,6 +13,7 @@ function getJSONObj(json_input) { class User { constructor() { this.UserId = -1; + this.DefaultCurrency = -1; this.Name = ""; this.Username = ""; this.Password = ""; @@ -21,6 +22,7 @@ class User { toJSON() { var json_obj = {}; json_obj.UserId = this.UserId; + json_obj.DefaultCurrency = this.DefaultCurrency; json_obj.Name = this.Name; json_obj.Username = this.Username; json_obj.Password = this.Password; @@ -32,6 +34,8 @@ class User { if (json_obj.hasOwnProperty("UserId")) this.UserId = json_obj.UserId; + if (json_obj.hasOwnProperty("DefaultCurrency")) + this.DefaultCurrency = json_obj.DefaultCurrency; if (json_obj.hasOwnProperty("Name")) this.Name = json_obj.Name; if (json_obj.hasOwnProperty("Username")) diff --git a/js/reducers/SecurityTemplateReducer.js b/js/reducers/SecurityTemplateReducer.js index b1ade5a..9e0b0a7 100644 --- a/js/reducers/SecurityTemplateReducer.js +++ b/js/reducers/SecurityTemplateReducer.js @@ -3,30 +3,37 @@ var assign = require('object-assign'); var SecurityTemplateConstants = require('../constants/SecurityTemplateConstants'); var UserConstants = require('../constants/UserConstants'); -var SecurityType = require('../models').SecurityType; +const initialState = { + search: "", + type: 0, + templates: [], + currencies: [] +}; -module.exports = function(state = {search: "", type: 0, templates: [], searchNumber: 0}, action) { +module.exports = function(state = initialState, action) { switch (action.type) { case SecurityTemplateConstants.SEARCH_SECURITY_TEMPLATES: - return { + return assign({}, state, { search: action.searchString, type: action.searchType, templates: [] - }; + }); case SecurityTemplateConstants.SECURITY_TEMPLATES_SEARCHED: if ((action.searchString != state.search) || (action.searchType != state.type)) return state; - return { + return assign({}, state, { search: action.searchString, type: action.searchType, templates: action.securities - }; + }); + case SecurityTemplateConstants.CURRENCIES_FETCHED: + return assign({}, state, { + currencies: action.currencies + }); case UserConstants.USER_LOGGEDOUT: - return { - search: "", - type: 0, - templates: [] - }; + return assign({}, initialState, { + currencies: state.currencies + }); default: return state; } diff --git a/securities.go b/securities.go index 9843825..f670824 100644 --- a/securities.go +++ b/securities.go @@ -36,7 +36,7 @@ type Security struct { // security is precise to Precision int Type int64 - // AlternateId is CUSIP for Type=Stock + // AlternateId is CUSIP for Type=Stock, ISO4217 for Type=Currency AlternateId string } @@ -86,6 +86,16 @@ func FindSecurityTemplate(name string, _type int64) *Security { return nil } +func FindCurrencyTemplate(iso4217 int64) *Security { + iso4217string := strconv.FormatInt(iso4217, 10) + for _, security := range SecurityTemplates { + if security.Type == Currency && security.AlternateId == iso4217string { + return &security + } + } + return nil +} + func GetSecurity(securityid int64, userid int64) (*Security, error) { var s Security @@ -171,6 +181,15 @@ func DeleteSecurity(s *Security) error { return errors.New("One or more accounts still use this security") } + user, err := GetUserTx(transaction, s.UserId) + if err != nil { + transaction.Rollback() + return err + } else if user.DefaultCurrency == s.SecurityId { + transaction.Rollback() + return errors.New("Cannot delete security which is user's default currency") + } + count, err := transaction.Delete(s) if err != nil { transaction.Rollback() diff --git a/users.go b/users.go index 3629c3c..e994188 100644 --- a/users.go +++ b/users.go @@ -3,7 +3,9 @@ package main import ( "crypto/sha256" "encoding/json" + "errors" "fmt" + "gopkg.in/gorp.v1" "io" "log" "net/http" @@ -11,12 +13,13 @@ import ( ) type User struct { - UserId int64 - Name string - Username string - Password string `db:"-"` - PasswordHash string `json:"-"` - Email string + UserId int64 + DefaultCurrency int64 // SecurityId of default currency, or ISO4217 code for it if creating new user + Name string + Username string + Password string `db:"-"` + PasswordHash string `json:"-"` + Email string } const BogusPassword = "password" @@ -54,6 +57,16 @@ func GetUser(userid int64) (*User, error) { return &u, nil } +func GetUserTx(transaction *gorp.Transaction, userid int64) (*User, error) { + var u User + + err := transaction.SelectOne(&u, "SELECT * from users where UserId=?", userid) + if err != nil { + return nil, err + } + return &u, nil +} + func GetUserByUsername(username string) (*User, error) { var u User @@ -70,6 +83,12 @@ func InsertUser(u *User) error { return err } + security_template := FindCurrencyTemplate(u.DefaultCurrency) + if security_template == nil { + transaction.Rollback() + return errors.New("Invalid ISO4217 Default Currency") + } + existing, err := transaction.SelectInt("SELECT count(*) from users where Username=?", u.Username) if err != nil { transaction.Rollback() @@ -86,6 +105,28 @@ func InsertUser(u *User) error { return err } + // Copy the security template and give it our new UserId + var security Security + security = *security_template + security.UserId = u.UserId + + err = InsertSecurityTx(transaction, &security) + if err != nil { + transaction.Rollback() + return err + } + + // Update the user's DefaultCurrency to our new SecurityId + u.DefaultCurrency = security.SecurityId + count, err := transaction.Update(u) + if err != nil { + transaction.Rollback() + return err + } else if count != 1 { + transaction.Rollback() + return errors.New("Would have updated more than one user") + } + err = transaction.Commit() if err != nil { transaction.Rollback() @@ -103,6 +144,42 @@ func GetUserFromSession(r *http.Request) (*User, error) { return GetUser(s.UserId) } +func UpdateUser(u *User) error { + transaction, err := DB.Begin() + if err != nil { + return err + } + + security, err := GetSecurityTx(transaction, u.DefaultCurrency, u.UserId) + if err != nil { + transaction.Rollback() + return err + } else if security.UserId != u.UserId || security.SecurityId != u.DefaultCurrency { + transaction.Rollback() + return errors.New("UserId and DefaultCurrency don't match the fetched security") + } else if security.Type != Currency { + transaction.Rollback() + return errors.New("New DefaultCurrency security is not a currency") + } + + count, err := transaction.Update(u) + if err != nil { + transaction.Rollback() + return err + } else if count != 1 { + transaction.Rollback() + return errors.New("Would have updated more than one user") + } + + err = transaction.Commit() + if err != nil { + transaction.Rollback() + return err + } + + return nil +} + func UserHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { user_json := r.PostFormValue("user") @@ -187,8 +264,8 @@ func UserHandler(w http.ResponseWriter, r *http.Request) { user.PasswordHash = old_pwhash } - count, err := DB.Update(user) - if count != 1 || err != nil { + err = UpdateUser(user) + if err != nil { WriteError(w, 999 /*Internal Error*/) log.Print(err) return