Add per-user default currency

This commit is contained in:
Aaron Lindsay 2017-06-21 21:25:38 -04:00
parent 25b04a4f0f
commit 4e73e8b508
12 changed files with 237 additions and 23 deletions

View File

@ -5,6 +5,7 @@ var ErrorActions = require('./ErrorActions');
var models = require('../models.js'); var models = require('../models.js');
var Security = models.Security; var Security = models.Security;
var Error = models.Error; var Error = models.Error;
var SecurityType = models.SecurityType;
function searchSecurityTemplates(searchString, searchType) { function searchSecurityTemplates(searchString, searchType) {
return { 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) { function search(searchString, searchType, limit) {
return function (dispatch) { return function (dispatch) {
dispatch(searchSecurityTemplates(searchString, searchType)); 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 = { module.exports = {
search: search search: search,
fetchCurrencies: fetchCurrencies
}; };

View File

@ -12,6 +12,8 @@ var FormControl = ReactBootstrap.FormControl;
var ControlLabel = ReactBootstrap.ControlLabel; var ControlLabel = ReactBootstrap.ControlLabel;
var Col = ReactBootstrap.Col; var Col = ReactBootstrap.Col;
var Combobox = require('react-widgets').Combobox;
var models = require('../models'); var models = require('../models');
var User = models.User; var User = models.User;
@ -22,6 +24,7 @@ class AccountSettingsModal extends React.Component {
name: props ? props.user.Name: "", name: props ? props.user.Name: "",
username: props ? props.user.Username : "", username: props ? props.user.Username : "",
email: props ? props.user.Email : "", email: props ? props.user.Email : "",
defaultCurrency: props ? props.user.DefaultCurrency : "",
password: models.BogusPassword, password: models.BogusPassword,
confirm_password: models.BogusPassword, confirm_password: models.BogusPassword,
passwordChanged: false, passwordChanged: false,
@ -33,6 +36,7 @@ class AccountSettingsModal extends React.Component {
this.state = this._getInitialState(); this.state = this._getInitialState();
this.onCancel = this.handleCancel.bind(this); this.onCancel = this.handleCancel.bind(this);
this.onChange = this.handleChange.bind(this); this.onChange = this.handleChange.bind(this);
this.onSelectCurrency = this.handleSelectCurrency.bind(this);
this.onSubmit = this.handleSubmit.bind(this); this.onSubmit = this.handleSubmit.bind(this);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
@ -73,6 +77,13 @@ class AccountSettingsModal extends React.Component {
confirm_password: ReactDOM.findDOMNode(this.refs.confirm_password).value confirm_password: ReactDOM.findDOMNode(this.refs.confirm_password).value
}); });
} }
handleSelectCurrency(security) {
if (security.hasOwnProperty('SecurityId')) {
this.setState({
defaultCurrency: security.SecurityId
});
}
}
handleSubmit(e) { handleSubmit(e) {
var u = new User(); var u = new User();
e.preventDefault(); e.preventDefault();
@ -81,6 +92,7 @@ class AccountSettingsModal extends React.Component {
u.Name = this.state.name; u.Name = this.state.name;
u.Username = this.state.username; u.Username = this.state.username;
u.Email = this.state.email; u.Email = this.state.email;
u.DefaultCurrency = this.state.defaultCurrency;
if (this.state.passwordChanged) { if (this.state.passwordChanged) {
u.Password = this.state.password; u.Password = this.state.password;
if (u.Password != this.state.confirm_password) { if (u.Password != this.state.confirm_password) {
@ -130,6 +142,20 @@ class AccountSettingsModal extends React.Component {
ref="email"/> ref="email"/>
</Col> </Col>
</FormGroup> </FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} xs={2}>Default Currency</Col>
<Col xs={10}>
<Combobox
data={this.props.currencies}
valueField='SecurityId'
textField={item => item == undefined || typeof item === 'string' ? item : item.Name + " - " + item.Description}
defaultValue={this.state.defaultCurrency}
onChange={this.onSelectCurrency}
suggest
filter='contains'
ref="security" />
</Col>
</FormGroup>
<FormGroup validationState={this.passwordValidationState()}> <FormGroup validationState={this.passwordValidationState()}>
<Col componentClass={ControlLabel} xs={2}>Password</Col> <Col componentClass={ControlLabel} xs={2}>Password</Col>
<Col xs={10}> <Col xs={10}>

View File

@ -27,6 +27,7 @@ class MoneyGoApp extends React.Component {
} }
componentDidMount() { componentDidMount() {
this.props.tryResumingSession(); this.props.tryResumingSession();
this.props.fetchCurrencies();
} }
handleShowSettings() { handleShowSettings() {
this.setState({showAccountSettingsModal: true}); this.setState({showAccountSettingsModal: true});

View File

@ -11,6 +11,8 @@ var Col = ReactBootstrap.Col;
var Button = ReactBootstrap.Button; var Button = ReactBootstrap.Button;
var ButtonGroup = ReactBootstrap.ButtonGroup; var ButtonGroup = ReactBootstrap.ButtonGroup;
var Combobox = require('react-widgets').Combobox;
var models = require('../models'); var models = require('../models');
var User = models.User; var User = models.User;
@ -22,6 +24,7 @@ class NewUserModal extends React.Component {
name: "", name: "",
username: "", username: "",
email: "", email: "",
defaultCurrency: '840', // ISO4217 code for USD
password: "", password: "",
confirm_password: "", confirm_password: "",
passwordChanged: false, passwordChanged: false,
@ -29,6 +32,7 @@ class NewUserModal extends React.Component {
}; };
this.onCancel = this.handleCancel.bind(this); this.onCancel = this.handleCancel.bind(this);
this.onChange = this.handleChange.bind(this); this.onChange = this.handleChange.bind(this);
this.onSelectCurrency = this.handleSelectCurrency.bind(this);
this.onSubmit = this.handleSubmit.bind(this); this.onSubmit = this.handleSubmit.bind(this);
} }
passwordValidationState() { passwordValidationState() {
@ -64,6 +68,13 @@ class NewUserModal extends React.Component {
confirm_password: ReactDOM.findDOMNode(this.refs.confirm_password).value confirm_password: ReactDOM.findDOMNode(this.refs.confirm_password).value
}); });
} }
handleSelectCurrency(security) {
if (security.hasOwnProperty('SecurityId')) {
this.setState({
defaultCurrency: security.AlternateId
});
}
}
handleSubmit(e) { handleSubmit(e) {
var u = new User(); var u = new User();
var error = ""; var error = "";
@ -72,6 +83,7 @@ class NewUserModal extends React.Component {
u.Name = this.state.name; u.Name = this.state.name;
u.Username = this.state.username; u.Username = this.state.username;
u.Email = this.state.email; u.Email = this.state.email;
u.DefaultCurrency = Number.parseInt(this.state.defaultCurrency);
u.Password = this.state.password; u.Password = this.state.password;
if (u.Password != this.state.confirm_password) { if (u.Password != this.state.confirm_password) {
this.setState({error: "Error: passwords do not match"}); this.setState({error: "Error: passwords do not match"});
@ -118,6 +130,20 @@ class NewUserModal extends React.Component {
ref="email"/> ref="email"/>
</Col> </Col>
</FormGroup> </FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} xs={2}>Default Currency</Col>
<Col xs={10}>
<Combobox
data={this.props.currencies}
valueField='AlternateId'
textField={item => typeof item === 'string' ? item : item.Name + " - " + item.Description}
defaultValue={this.state.defaultCurrency}
onChange={this.onSelectCurrency}
suggest
filter='contains'
ref="security" />
</Col>
</FormGroup>
<FormGroup validationState={this.passwordValidationState()}> <FormGroup validationState={this.passwordValidationState()}>
<Col componentClass={ControlLabel} xs={2}>Password</Col> <Col componentClass={ControlLabel} xs={2}>Password</Col>
<Col xs={10}> <Col xs={10}>

View File

@ -1,6 +1,8 @@
var keyMirror = require('keymirror'); var keyMirror = require('keymirror');
module.exports = keyMirror({ module.exports = keyMirror({
FETCH_CURRENCIES: null,
CURRENCIES_FETCHED: null,
SEARCH_SECURITY_TEMPLATES: null, SEARCH_SECURITY_TEMPLATES: null,
SECURITY_TEMPLATES_SEARCHED: null SECURITY_TEMPLATES_SEARCHED: null
}); });

View File

@ -1,11 +1,13 @@
var connect = require('react-redux').connect; var connect = require('react-redux').connect;
var UserActions = require('../actions/UserActions'); var UserActions = require('../actions/UserActions');
var AccountSettingsModal = require('../components/AccountSettingsModal'); var AccountSettingsModal = require('../components/AccountSettingsModal');
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
user: state.user user: state.user,
currencies: state.securities.currency_list
} }
} }

View File

@ -1,6 +1,7 @@
var connect = require('react-redux').connect; var connect = require('react-redux').connect;
var UserActions = require('../actions/UserActions'); var UserActions = require('../actions/UserActions');
var SecurityTemplateActions = require('../actions/SecurityTemplateActions');
var MoneyGoApp = require('../components/MoneyGoApp'); var MoneyGoApp = require('../components/MoneyGoApp');
@ -13,6 +14,7 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
tryResumingSession: function() {dispatch(UserActions.tryResumingSession())}, tryResumingSession: function() {dispatch(UserActions.tryResumingSession())},
fetchCurrencies: function() {dispatch(SecurityTemplateActions.fetchCurrencies())},
} }
} }

View File

@ -5,7 +5,9 @@ var UserActions = require('../actions/UserActions');
var NewUserModal = require('../components/NewUserModal'); var NewUserModal = require('../components/NewUserModal');
function mapStateToProps(state) { function mapStateToProps(state) {
return {} return {
currencies: state.securityTemplates.currencies
}
} }
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {

View File

@ -13,6 +13,7 @@ function getJSONObj(json_input) {
class User { class User {
constructor() { constructor() {
this.UserId = -1; this.UserId = -1;
this.DefaultCurrency = -1;
this.Name = ""; this.Name = "";
this.Username = ""; this.Username = "";
this.Password = ""; this.Password = "";
@ -21,6 +22,7 @@ class User {
toJSON() { toJSON() {
var json_obj = {}; var json_obj = {};
json_obj.UserId = this.UserId; json_obj.UserId = this.UserId;
json_obj.DefaultCurrency = this.DefaultCurrency;
json_obj.Name = this.Name; json_obj.Name = this.Name;
json_obj.Username = this.Username; json_obj.Username = this.Username;
json_obj.Password = this.Password; json_obj.Password = this.Password;
@ -32,6 +34,8 @@ class User {
if (json_obj.hasOwnProperty("UserId")) if (json_obj.hasOwnProperty("UserId"))
this.UserId = json_obj.UserId; this.UserId = json_obj.UserId;
if (json_obj.hasOwnProperty("DefaultCurrency"))
this.DefaultCurrency = json_obj.DefaultCurrency;
if (json_obj.hasOwnProperty("Name")) if (json_obj.hasOwnProperty("Name"))
this.Name = json_obj.Name; this.Name = json_obj.Name;
if (json_obj.hasOwnProperty("Username")) if (json_obj.hasOwnProperty("Username"))

View File

@ -3,30 +3,37 @@ var assign = require('object-assign');
var SecurityTemplateConstants = require('../constants/SecurityTemplateConstants'); var SecurityTemplateConstants = require('../constants/SecurityTemplateConstants');
var UserConstants = require('../constants/UserConstants'); 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) { switch (action.type) {
case SecurityTemplateConstants.SEARCH_SECURITY_TEMPLATES: case SecurityTemplateConstants.SEARCH_SECURITY_TEMPLATES:
return { return assign({}, state, {
search: action.searchString, search: action.searchString,
type: action.searchType, type: action.searchType,
templates: [] templates: []
}; });
case SecurityTemplateConstants.SECURITY_TEMPLATES_SEARCHED: case SecurityTemplateConstants.SECURITY_TEMPLATES_SEARCHED:
if ((action.searchString != state.search) || (action.searchType != state.type)) if ((action.searchString != state.search) || (action.searchType != state.type))
return state; return state;
return { return assign({}, state, {
search: action.searchString, search: action.searchString,
type: action.searchType, type: action.searchType,
templates: action.securities templates: action.securities
}; });
case SecurityTemplateConstants.CURRENCIES_FETCHED:
return assign({}, state, {
currencies: action.currencies
});
case UserConstants.USER_LOGGEDOUT: case UserConstants.USER_LOGGEDOUT:
return { return assign({}, initialState, {
search: "", currencies: state.currencies
type: 0, });
templates: []
};
default: default:
return state; return state;
} }

View File

@ -36,7 +36,7 @@ type Security struct {
// security is precise to // security is precise to
Precision int Precision int
Type int64 Type int64
// AlternateId is CUSIP for Type=Stock // AlternateId is CUSIP for Type=Stock, ISO4217 for Type=Currency
AlternateId string AlternateId string
} }
@ -86,6 +86,16 @@ func FindSecurityTemplate(name string, _type int64) *Security {
return nil 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) { func GetSecurity(securityid int64, userid int64) (*Security, error) {
var s Security var s Security
@ -171,6 +181,15 @@ func DeleteSecurity(s *Security) error {
return errors.New("One or more accounts still use this security") 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) count, err := transaction.Delete(s)
if err != nil { if err != nil {
transaction.Rollback() transaction.Rollback()

View File

@ -3,7 +3,9 @@ package main
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"gopkg.in/gorp.v1"
"io" "io"
"log" "log"
"net/http" "net/http"
@ -11,12 +13,13 @@ import (
) )
type User struct { type User struct {
UserId int64 UserId int64
Name string DefaultCurrency int64 // SecurityId of default currency, or ISO4217 code for it if creating new user
Username string Name string
Password string `db:"-"` Username string
PasswordHash string `json:"-"` Password string `db:"-"`
Email string PasswordHash string `json:"-"`
Email string
} }
const BogusPassword = "password" const BogusPassword = "password"
@ -54,6 +57,16 @@ func GetUser(userid int64) (*User, error) {
return &u, nil 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) { func GetUserByUsername(username string) (*User, error) {
var u User var u User
@ -70,6 +83,12 @@ func InsertUser(u *User) error {
return err 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) existing, err := transaction.SelectInt("SELECT count(*) from users where Username=?", u.Username)
if err != nil { if err != nil {
transaction.Rollback() transaction.Rollback()
@ -86,6 +105,28 @@ func InsertUser(u *User) error {
return err 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() err = transaction.Commit()
if err != nil { if err != nil {
transaction.Rollback() transaction.Rollback()
@ -103,6 +144,42 @@ func GetUserFromSession(r *http.Request) (*User, error) {
return GetUser(s.UserId) 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) { func UserHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" { if r.Method == "POST" {
user_json := r.PostFormValue("user") user_json := r.PostFormValue("user")
@ -187,8 +264,8 @@ func UserHandler(w http.ResponseWriter, r *http.Request) {
user.PasswordHash = old_pwhash user.PasswordHash = old_pwhash
} }
count, err := DB.Update(user) err = UpdateUser(user)
if count != 1 || err != nil { if err != nil {
WriteError(w, 999 /*Internal Error*/) WriteError(w, 999 /*Internal Error*/)
log.Print(err) log.Print(err)
return return