diff --git a/js/actions/SecurityActions.js b/js/actions/SecurityActions.js
index 268dac4..1d92aa4 100644
--- a/js/actions/SecurityActions.js
+++ b/js/actions/SecurityActions.js
@@ -19,6 +19,52 @@ function securitiesFetched(securities) {
}
}
+function createSecurity() {
+ return {
+ type: SecurityConstants.CREATE_SECURITY
+ }
+}
+
+function securityCreated(security) {
+ return {
+ type: SecurityConstants.SECURITY_CREATED,
+ security: security
+ }
+}
+
+function updateSecurity() {
+ return {
+ type: SecurityConstants.UPDATE_SECURITY
+ }
+}
+
+function securityUpdated(security) {
+ return {
+ type: SecurityConstants.SECURITY_UPDATED,
+ security: security
+ }
+}
+
+function removeSecurity() {
+ return {
+ type: SecurityConstants.REMOVE_SECURITY
+ }
+}
+
+function securityRemoved(securityId) {
+ return {
+ type: SecurityConstants.SECURITY_REMOVED,
+ securityId: securityId
+ }
+}
+
+function securitySelected(securityId) {
+ return {
+ type: SecurityConstants.SECURITY_SELECTED,
+ securityId: securityId
+ }
+}
+
function fetchAll() {
return function (dispatch) {
dispatch(fetchSecurities());
@@ -47,6 +93,88 @@ function fetchAll() {
};
}
+function create(security) {
+ return function (dispatch) {
+ dispatch(createSecurity());
+
+ $.ajax({
+ type: "POST",
+ dataType: "json",
+ url: "security/",
+ data: {security: security.toJSON()},
+ success: function(data, status, jqXHR) {
+ var e = new Error();
+ e.fromJSON(data);
+ if (e.isError()) {
+ ErrorActions.serverError(e);
+ } else {
+ var s = new Security();
+ s.fromJSON(data);
+ dispatch(securityCreated(s));
+ }
+ },
+ error: function(jqXHR, status, error) {
+ ErrorActions.ajaxError(e);
+ }
+ });
+ };
+}
+
+function update(security) {
+ return function (dispatch) {
+ dispatch(updateSecurity());
+
+ $.ajax({
+ type: "PUT",
+ dataType: "json",
+ url: "security/"+security.SecurityId+"/",
+ data: {security: security.toJSON()},
+ success: function(data, status, jqXHR) {
+ var e = new Error();
+ e.fromJSON(data);
+ if (e.isError()) {
+ ErrorActions.serverError(e);
+ } else {
+ var s = new Security();
+ s.fromJSON(data);
+ dispatch(securityUpdated(s));
+ }
+ },
+ error: function(jqXHR, status, error) {
+ ErrorActions.ajaxError(e);
+ }
+ });
+ };
+}
+
+function remove(security) {
+ return function(dispatch) {
+ dispatch(removeSecurity());
+
+ $.ajax({
+ type: "DELETE",
+ dataType: "json",
+ url: "security/"+security.SecurityId+"/",
+ success: function(data, status, jqXHR) {
+ var e = new Error();
+ e.fromJSON(data);
+ if (e.isError()) {
+ ErrorActions.serverError(e);
+ } else {
+ dispatch(securityRemoved(security.SecurityId));
+ }
+ },
+ error: function(jqXHR, status, error) {
+ ErrorActions.ajaxError(e);
+ }
+ });
+ };
+}
+
module.exports = {
- fetchAll: fetchAll
+ fetchAll: fetchAll,
+ create: create,
+ update: update,
+ remove: remove,
+ select: securitySelected
};
diff --git a/js/actions/SecurityTemplateActions.js b/js/actions/SecurityTemplateActions.js
new file mode 100644
index 0000000..7d30db2
--- /dev/null
+++ b/js/actions/SecurityTemplateActions.js
@@ -0,0 +1,62 @@
+var SecurityTemplateConstants = require('../constants/SecurityTemplateConstants');
+
+var ErrorActions = require('./ErrorActions');
+
+var models = require('../models.js');
+var Security = models.Security;
+var Error = models.Error;
+
+function searchSecurityTemplates(searchString, searchType) {
+ return {
+ type: SecurityTemplateConstants.SEARCH_SECURITY_TEMPLATES,
+ searchString: searchString,
+ searchType: searchType
+ }
+}
+
+function securityTemplatesSearched(searchString, searchType, securities) {
+ return {
+ type: SecurityTemplateConstants.SECURITY_TEMPLATES_SEARCHED,
+ searchString: searchString,
+ searchType: searchType,
+ securities: securities
+ }
+}
+
+function search(searchString, searchType, limit) {
+ return function (dispatch) {
+ dispatch(searchSecurityTemplates(searchString, searchType));
+
+ if (searchString == "")
+ return;
+
+ $.ajax({
+ type: "GET",
+ dataType: "json",
+ url: "securitytemplate/?search="+searchString+"&type="+searchType+"&limit="+limit,
+ success: function(data, status, jqXHR) {
+ var e = new Error();
+ e.fromJSON(data);
+ if (e.isError()) {
+ ErrorActions.serverError(e);
+ } else if (data.securities == null) {
+ dispatch(securityTemplatesSearched(searchString, searchType, new Array()));
+ } else {
+ dispatch(securityTemplatesSearched(searchString, searchType,
+ data.securities.map(function(json) {
+ var s = new Security();
+ s.fromJSON(json);
+ return s;
+ })));
+ }
+ },
+ error: function(jqXHR, status, error) {
+ ErrorActions.ajaxError(e);
+ }
+ });
+ };
+}
+
+module.exports = {
+ search: search
+};
diff --git a/js/components/AccountsTab.js b/js/components/AccountsTab.js
index b585ef5..de2866d 100644
--- a/js/components/AccountsTab.js
+++ b/js/components/AccountsTab.js
@@ -15,7 +15,6 @@ var ButtonGroup = ReactBootstrap.ButtonGroup;
var Glyphicon = ReactBootstrap.Glyphicon;
var ListGroup = ReactBootstrap.ListGroup;
var ListGroupItem = ReactBootstrap.ListGroupItem;
-var Collapse = ReactBootstrap.Collapse;
var Alert = ReactBootstrap.Alert;
var Modal = ReactBootstrap.Modal;
var Collapse = ReactBootstrap.Collapse;
diff --git a/js/components/MoneyGoApp.js b/js/components/MoneyGoApp.js
index 4f21f04..98d27de 100644
--- a/js/components/MoneyGoApp.js
+++ b/js/components/MoneyGoApp.js
@@ -10,6 +10,7 @@ var TopBarContainer = require('../containers/TopBarContainer');
var NewUserForm = require('./NewUserForm');
var AccountSettingsModalContainer = require('../containers/AccountSettingsModalContainer');
var AccountsTabContainer = require('../containers/AccountsTabContainer');
+var SecuritiesTabContainer = require('../containers/SecuritiesTabContainer');
module.exports = React.createClass({
displayName: "MoneyGoApp",
@@ -67,9 +68,13 @@ module.exports = React.createClass({
- Scheduled transactions go here...
- Budgets go here...
- Reports go here...
+
+
+
+ Scheduled transactions go here...
+ Budgets go here...
+ Reports go here...
);
else
mainContent = (
diff --git a/js/components/SecuritiesTab.js b/js/components/SecuritiesTab.js
new file mode 100644
index 0000000..f842d6c
--- /dev/null
+++ b/js/components/SecuritiesTab.js
@@ -0,0 +1,363 @@
+var React = require('react');
+var ReactDOM = require('react-dom');
+
+var ReactBootstrap = require('react-bootstrap');
+var Grid = ReactBootstrap.Grid;
+var Row = ReactBootstrap.Row;
+var Col = ReactBootstrap.Col;
+var Form = ReactBootstrap.Form;
+var FormGroup = ReactBootstrap.FormGroup;
+var FormControl = ReactBootstrap.FormControl;
+var ControlLabel = ReactBootstrap.ControlLabel;
+var Button = ReactBootstrap.Button;
+var ButtonGroup = ReactBootstrap.ButtonGroup;
+var ButtonToolbar = ReactBootstrap.ButtonToolbar;
+var Glyphicon = ReactBootstrap.Glyphicon;
+var ListGroup = ReactBootstrap.ListGroup;
+var ListGroupItem = ReactBootstrap.ListGroupItem;
+var Modal = ReactBootstrap.Modal;
+var Panel = ReactBootstrap.Panel;
+
+var Combobox = require('react-widgets').Combobox;
+
+var models = require('../models');
+var Security = models.Security;
+var SecurityType = models.SecurityType;
+var SecurityTypeList = models.SecurityTypeList;
+
+const SecurityTemplatePanel = React.createClass({
+ handleSearchChange: function(){
+ this.props.onSearchTemplates(ReactDOM.findDOMNode(this.refs.search).value, 0, this.props.maxResults + 1);
+ },
+ renderTemplateList: function() {
+ var templates = this.props.securityTemplates;
+ if (this.props.search != "") {
+ var items = [];
+ for (var i = 0; i < templates.length && i < 15; i++) {
+ var template = templates[i];
+ var self = this;
+ var onClickFn = (function() {
+ var j = i;
+ return function(){self.props.onSelectTemplate(templates[j])};
+ })();
+ var key = template.Type.toString() + template.AlternateId;
+ items.push((
+
+ {template.Name} - {template.Description}
+
+ ));
+ }
+ if (templates.length > this.props.maxResults) {
+ items.push((
+
+ Too many templates to display, please refine your search...
+
+ ));
+ } else if (templates.length == 0) {
+ items.push((
+
+ Sorry, no templates matched your search...
+
+ ));
+ }
+ return (
+
+
+ Select a template to populate your security:
+
+ {items}
+
+
+ );
+ }
+ },
+ render: function() {
+ return (
+
+
+ {this.renderTemplateList()}
+
+ );
+ }
+});
+
+const AddEditSecurityModal = React.createClass({
+ getInitialState: function() {
+ var s = {
+ securityid: -1,
+ name: "",
+ description: "",
+ symbol: "",
+ precision: 0,
+ type: 1,
+ alternateid: ""
+ };
+ if (this.props.editSecurity != null) {
+ s.securityid = this.props.editSecurity.SecurityId;
+ s.name = this.props.editSecurity.Name;
+ s.description = this.props.editSecurity.Description;
+ s.symbol = this.props.editSecurity.Symbol;
+ s.precision = this.props.editSecurity.Precision;
+ s.type = this.props.editSecurity.Type;
+ s.alternateid = this.props.editSecurity.AlternateId;
+ }
+ return s;
+ },
+ onSelectTemplate: function(template) {
+ this.setState({
+ name: template.Name,
+ description: template.Description,
+ symbol: template.Symbol,
+ precision: template.Precision,
+ type: template.Type,
+ alternateid: template.AlternateId
+ });
+ },
+ handleCancel: function() {
+ if (this.props.onCancel != null)
+ this.props.onCancel();
+ },
+ handleNameChange: function() {
+ this.setState({
+ name: ReactDOM.findDOMNode(this.refs.name).value,
+ });
+ },
+ handleDescriptionChange: function() {
+ this.setState({
+ description: ReactDOM.findDOMNode(this.refs.description).value,
+ });
+ },
+ handleSymbolChange: function() {
+ this.setState({
+ symbol: ReactDOM.findDOMNode(this.refs.symbol).value,
+ });
+ },
+ handlePrecisionChange: function() {
+ this.setState({
+ precision: +ReactDOM.findDOMNode(this.refs.precision).value,
+ });
+ },
+ handleTypeChange: function(type) {
+ if (type.hasOwnProperty('TypeId'))
+ this.setState({
+ type: type.TypeId
+ });
+ },
+ handleAlternateIdChange: function() {
+ this.setState({
+ alternateid: ReactDOM.findDOMNode(this.refs.alternateid).value,
+ });
+ },
+ handleSubmit: function() {
+ var s = new Security();
+
+ if (this.props.editSecurity != null)
+ s.SecurityId = this.state.securityid;
+ s.Name = this.state.name;
+ s.Description = this.state.description;
+ s.Symbol = this.state.symbol;
+ s.Precision = this.state.precision;
+ s.Type = this.state.type;
+ s.AlternateId = this.state.alternateid;
+
+ if (this.props.onSubmit != null)
+ this.props.onSubmit(s);
+ },
+ componentWillReceiveProps: function(nextProps) {
+ if (nextProps.show && !this.props.show) {
+ this.setState(this.getInitialState());
+ }
+ },
+ render: function() {
+ var headerText = (this.props.editSecurity != null) ? "Edit" : "Create New";
+ var buttonText = (this.props.editSecurity != null) ? "Save Changes" : "Create Security";
+ var alternateidname = (this.state.type == SecurityType.Currency) ? "ISO 4217 Code" : "CUSIP";
+ return (
+
+
+ {headerText} Security
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+});
+
+const SecurityList = React.createClass({
+ render: function() {
+ var children = [];
+ var self = this;
+ for (var securityId in this.props.securities) {
+ if (this.props.securities.hasOwnProperty(securityId)) {
+ var buttonStyle = (securityId == this.props.selectedSecurity) ? "info" : "link";
+ var onClickFn = (function() {
+ var id = securityId;
+ return function(){self.props.onSelectSecurity(id)};
+ })();
+ children.push(());
+ }
+ }
+
+ return (
+
+ {children}
+
+ );
+ }
+});
+
+module.exports = React.createClass({
+ displayName: "SecuritiesTab",
+ getInitialState: function() {
+ return {
+ creatingNewSecurity: false,
+ editingSecurity: false
+ };
+ },
+ handleNewSecurity: function() {
+ this.setState({creatingNewSecurity: true});
+ },
+ handleEditSecurity: function() {
+ this.setState({editingSecurity: true});
+ },
+ handleCreationCancel: function() {
+ this.setState({creatingNewSecurity: false});
+ },
+ handleCreationSubmit: function(security) {
+ this.setState({creatingNewSecurity: false});
+ this.props.onCreateSecurity(security);
+ },
+ handleEditingCancel: function() {
+ this.setState({editingSecurity: false});
+ },
+ handleEditingSubmit: function(security) {
+ this.setState({editingSecurity: false});
+ this.props.onUpdateSecurity(security);
+ },
+ render: function() {
+ var editDisabled = this.props.selectedSecurity == -1;
+
+ var selectedSecurity = null;
+ if (this.props.securities.hasOwnProperty(this.props.selectedSecurity))
+ selectedSecurity = this.props.securities[this.props.selectedSecurity];
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+});
diff --git a/js/constants/SecurityConstants.js b/js/constants/SecurityConstants.js
index 5c38b5b..a013df9 100644
--- a/js/constants/SecurityConstants.js
+++ b/js/constants/SecurityConstants.js
@@ -8,5 +8,6 @@ module.exports = keyMirror({
UPDATE_SECURITY: null,
SECURITY_UPDATED: null,
REMOVE_SECURITY: null,
- SECURITY_REMOVED: null
+ SECURITY_REMOVED: null,
+ SECURITY_SELECTED: null
});
diff --git a/js/constants/SecurityTemplateConstants.js b/js/constants/SecurityTemplateConstants.js
new file mode 100644
index 0000000..b8fc901
--- /dev/null
+++ b/js/constants/SecurityTemplateConstants.js
@@ -0,0 +1,6 @@
+var keyMirror = require('keymirror');
+
+module.exports = keyMirror({
+ SEARCH_SECURITY_TEMPLATES: null,
+ SECURITY_TEMPLATES_SEARCHED: null
+});
diff --git a/js/containers/SecuritiesTabContainer.js b/js/containers/SecuritiesTabContainer.js
new file mode 100644
index 0000000..7ab6eb7
--- /dev/null
+++ b/js/containers/SecuritiesTabContainer.js
@@ -0,0 +1,35 @@
+var connect = require('react-redux').connect;
+
+var SecurityActions = require('../actions/SecurityActions');
+var SecurityTemplateActions = require('../actions/SecurityTemplateActions');
+var SecuritiesTab = require('../components/SecuritiesTab');
+
+function mapStateToProps(state) {
+ var selectedSecurityAccounts = [];
+ for (var accountId in state.accounts) {
+ if (state.accounts.hasOwnProperty(accountId)
+ && state.accounts[accountId].SecurityId == state.selectedSecurity)
+ selectedSecurityAccounts.push(state.accounts[accountId]);
+ }
+ return {
+ securities: state.securities,
+ selectedSecurityAccounts: selectedSecurityAccounts,
+ selectedSecurity: state.selectedSecurity,
+ securityTemplates: state.securityTemplates
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ onCreateSecurity: function(security) {dispatch(SecurityActions.create(security))},
+ onUpdateSecurity: function(security) {dispatch(SecurityActions.update(security))},
+ onDeleteSecurity: function(securityId) {dispatch(SecurityActions.remove(securityId))},
+ onSelectSecurity: function(securityId) {dispatch(SecurityActions.select(securityId))},
+ onSearchTemplates: function(search, type, limit) {dispatch(SecurityTemplateActions.search(search, type, limit))}
+ }
+}
+
+module.exports = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(SecuritiesTab)
diff --git a/js/models.js b/js/models.js
index 51c8a8a..404ae07 100644
--- a/js/models.js
+++ b/js/models.js
@@ -94,6 +94,7 @@ function Security() {
this.Symbol = "";
this.Precision = -1;
this.Type = -1;
+ this.AlternateId = "";
}
Security.prototype.toJSON = function() {
@@ -104,6 +105,7 @@ Security.prototype.toJSON = function() {
json_obj.Symbol = this.Symbol;
json_obj.Precision = this.Precision;
json_obj.Type = this.Type;
+ json_obj.AlternateId = this.AlternateId;
return JSON.stringify(json_obj);
}
@@ -122,6 +124,8 @@ Security.prototype.fromJSON = function(json_input) {
this.Precision = json_obj.Precision;
if (json_obj.hasOwnProperty("Type"))
this.Type = json_obj.Type;
+ if (json_obj.hasOwnProperty("AlternateId"))
+ this.AlternateId = json_obj.AlternateId;
}
Security.prototype.isSecurity = function() {
diff --git a/js/reducers/MoneyGoReducer.js b/js/reducers/MoneyGoReducer.js
index 0c4e12e..4926973 100644
--- a/js/reducers/MoneyGoReducer.js
+++ b/js/reducers/MoneyGoReducer.js
@@ -4,7 +4,9 @@ var UserReducer = require('./UserReducer');
var SessionReducer = require('./SessionReducer');
var AccountReducer = require('./AccountReducer');
var SecurityReducer = require('./SecurityReducer');
+var SecurityTemplateReducer = require('./SecurityTemplateReducer');
var SelectedAccountReducer = require('./SelectedAccountReducer');
+var SelectedSecurityReducer = require('./SelectedSecurityReducer');
var ErrorReducer = require('./ErrorReducer');
module.exports = Redux.combineReducers({
@@ -12,6 +14,8 @@ module.exports = Redux.combineReducers({
session: SessionReducer,
accounts: AccountReducer,
securities: SecurityReducer,
+ securityTemplates: SecurityTemplateReducer,
selectedAccount: SelectedAccountReducer,
+ selectedSecurity: SelectedSecurityReducer,
error: ErrorReducer
});
diff --git a/js/reducers/SecurityTemplateReducer.js b/js/reducers/SecurityTemplateReducer.js
new file mode 100644
index 0000000..b1ade5a
--- /dev/null
+++ b/js/reducers/SecurityTemplateReducer.js
@@ -0,0 +1,33 @@
+var assign = require('object-assign');
+
+var SecurityTemplateConstants = require('../constants/SecurityTemplateConstants');
+var UserConstants = require('../constants/UserConstants');
+
+var SecurityType = require('../models').SecurityType;
+
+module.exports = function(state = {search: "", type: 0, templates: [], searchNumber: 0}, action) {
+ switch (action.type) {
+ case SecurityTemplateConstants.SEARCH_SECURITY_TEMPLATES:
+ return {
+ search: action.searchString,
+ type: action.searchType,
+ templates: []
+ };
+ case SecurityTemplateConstants.SECURITY_TEMPLATES_SEARCHED:
+ if ((action.searchString != state.search) || (action.searchType != state.type))
+ return state;
+ return {
+ search: action.searchString,
+ type: action.searchType,
+ templates: action.securities
+ };
+ case UserConstants.USER_LOGGEDOUT:
+ return {
+ search: "",
+ type: 0,
+ templates: []
+ };
+ default:
+ return state;
+ }
+};
diff --git a/js/reducers/SelectedSecurityReducer.js b/js/reducers/SelectedSecurityReducer.js
new file mode 100644
index 0000000..b792b80
--- /dev/null
+++ b/js/reducers/SelectedSecurityReducer.js
@@ -0,0 +1,23 @@
+var SecurityConstants = require('../constants/SecurityConstants');
+var UserConstants = require('../constants/UserConstants');
+
+module.exports = function(state = -1, action) {
+ switch (action.type) {
+ case SecurityConstants.SECURITIES_FETCHED:
+ for (var i = 0; i < action.securities.length; i++) {
+ if (action.securities[i].SecurityId == state)
+ return state;
+ }
+ return -1;
+ case SecurityConstants.SECURITY_REMOVED:
+ if (action.securityId == state)
+ return -1;
+ return state;
+ case SecurityConstants.SECURITY_SELECTED:
+ return action.securityId;
+ case UserConstants.USER_LOGGEDOUT:
+ return -1;
+ default:
+ return state;
+ }
+};
diff --git a/securities.go b/securities.go
index 03793f4..a56c94c 100644
--- a/securities.go
+++ b/securities.go
@@ -7,6 +7,7 @@ import (
"log"
"net/http"
"net/url"
+ "strconv"
"strings"
)
@@ -58,7 +59,7 @@ func (sl *SecurityList) Write(w http.ResponseWriter) error {
return enc.Encode(sl)
}
-func SearchSecurityTemplates(search string, _type int64) []*Security {
+func SearchSecurityTemplates(search string, _type int64, limit int64) []*Security {
upperSearch := strings.ToUpper(search)
var results []*Security
for i, security := range SecurityTemplates {
@@ -67,6 +68,9 @@ func SearchSecurityTemplates(search string, _type int64) []*Security {
strings.Contains(strings.ToUpper(security.Symbol), upperSearch) {
if _type == 0 || _type == security.Type {
results = append(results, &SecurityTemplates[i])
+ if limit != -1 && int64(len(results)) >= limit {
+ break
+ }
}
}
}
@@ -298,10 +302,22 @@ func SecurityTemplateHandler(w http.ResponseWriter, r *http.Request) {
var sl SecurityList
query, _ := url.ParseQuery(r.URL.RawQuery)
+
+ var limit int64 = -1
search := query.Get("search")
_type := GetSecurityType(query.Get("type"))
- securities := SearchSecurityTemplates(search, _type)
+ limitstring := query.Get("limit")
+ if limitstring != "" {
+ limitint, err := strconv.ParseInt(limitstring, 10, 0)
+ if err != nil {
+ WriteError(w, 3 /*Invalid Request*/)
+ return
+ }
+ limit = limitint
+ }
+
+ securities := SearchSecurityTemplates(search, _type, limit)
sl.Securities = &securities
err := (&sl).Write(w)
diff --git a/static/stylesheet.css b/static/stylesheet.css
index e6a8e77..cdc733a 100644
--- a/static/stylesheet.css
+++ b/static/stylesheet.css
@@ -61,7 +61,7 @@ div.accounttree-root div {
height: 100%-100px;
overflow: auto;
}
-.account-column {
+.account-column, .securitylist-column {
padding: 15px 15px 43px 15px;
border-right: 1px solid #DDD;
border-left: 1px solid #DDD;
@@ -82,7 +82,7 @@ div.accounttree-root div {
height: 100%;
overflow: auto;
}
-.transactions-column {
+.transactions-column, .securities-column {
padding: 15px;
border-right: 1px solid #DDD;
}