diff --git a/db.go b/db.go index 416d326..eb2cf1b 100644 --- a/db.go +++ b/db.go @@ -22,6 +22,7 @@ func initDB() *gorp.DbMap { dbmap.AddTableWithName(Security{}, "securities").SetKeys(true, "SecurityId") dbmap.AddTableWithName(Transaction{}, "transactions").SetKeys(true, "TransactionId") dbmap.AddTableWithName(Split{}, "splits").SetKeys(true, "SplitId") + dbmap.AddTableWithName(Report{}, "reports").SetKeys(true, "ReportId") err = dbmap.CreateTablesIfNotExists() if err != nil { diff --git a/js/actions/ReportActions.js b/js/actions/ReportActions.js index 3ca75d2..bc344ff 100644 --- a/js/actions/ReportActions.js +++ b/js/actions/ReportActions.js @@ -4,55 +4,116 @@ var ErrorActions = require('./ErrorActions'); var models = require('../models.js'); var Report = models.Report; +var Tabulation = models.Tabulation; var Error = models.Error; -function fetchReport(reportName) { +function fetchReports() { return { - type: ReportConstants.FETCH_REPORT, - reportName: reportName + type: ReportConstants.FETCH_REPORTS } } -function reportFetched(report) { +function reportsFetched(reports) { return { - type: ReportConstants.REPORT_FETCHED, + type: ReportConstants.REPORTS_FETCHED, + reports: reports + } +} + +function createReport() { + return { + type: ReportConstants.CREATE_REPORT + } +} + +function reportCreated(report) { + return { + type: ReportConstants.REPORT_CREATED, report: report } } -function selectReport(report, seriesTraversal) { +function updateReport() { return { - type: ReportConstants.SELECT_REPORT, - report: report, - seriesTraversal: seriesTraversal + type: ReportConstants.UPDATE_REPORT } } -function reportSelected(flattenedReport, seriesTraversal) { +function reportUpdated(report) { + return { + type: ReportConstants.REPORT_UPDATED, + report: report + } +} + +function removeReport() { + return { + type: ReportConstants.REMOVE_REPORT + } +} + +function reportRemoved(reportId) { + return { + type: ReportConstants.REPORT_REMOVED, + reportId: reportId + } +} + +function reportSelected(report) { return { type: ReportConstants.REPORT_SELECTED, - report: flattenedReport, + report: report + } +} + +function tabulateReport(report) { + return { + type: ReportConstants.TABULATE_REPORT, + report: report + } +} + +function reportTabulated(report, tabulation) { + return { + type: ReportConstants.REPORT_TABULATED, + report: report, + tabulation: tabulation + } +} + +function selectionCleared() { + return { + type: ReportConstants.SELECTION_CLEARED + } +} + +function seriesSelected(flattenedTabulation, seriesTraversal) { + return { + type: ReportConstants.SERIES_SELECTED, + tabulation: flattenedTabulation, seriesTraversal: seriesTraversal } } -function fetch(report) { +function fetchAll() { return function (dispatch) { - dispatch(fetchReport(report)); + dispatch(fetchReports()); $.ajax({ type: "GET", dataType: "json", - url: "report/"+report+"/", + url: "report/", success: function(data, status, jqXHR) { var e = new Error(); e.fromJSON(data); if (e.isError()) { dispatch(ErrorActions.serverError(e)); } else { - var r = new Report(); - r.fromJSON(data); - dispatch(reportFetched(r)); + dispatch(reportsFetched(data.reports.map(function(json) { + var r = new Report(); + r.fromJSON(json); + return r; + }))); } }, error: function(jqXHR, status, error) { @@ -62,14 +123,117 @@ function fetch(report) { }; } -function select(report, seriesTraversal) { +function create(report) { + return function (dispatch) { + dispatch(createReport()); + + $.ajax({ + type: "POST", + dataType: "json", + url: "report/", + data: {report: report.toJSON()}, + success: function(data, status, jqXHR) { + var e = new Error(); + e.fromJSON(data); + if (e.isError()) { + dispatch(ErrorActions.serverError(e)); + } else { + var a = new Report(); + a.fromJSON(data); + dispatch(reportCreated(a)); + } + }, + error: function(jqXHR, status, error) { + dispatch(ErrorActions.ajaxError(error)); + } + }); + }; +} + +function update(report) { + return function (dispatch) { + dispatch(updateReport()); + + $.ajax({ + type: "PUT", + dataType: "json", + url: "report/"+report.ReportId+"/", + data: {report: report.toJSON()}, + success: function(data, status, jqXHR) { + var e = new Error(); + e.fromJSON(data); + if (e.isError()) { + dispatch(ErrorActions.serverError(e)); + } else { + var a = new Report(); + a.fromJSON(data); + dispatch(reportUpdated(a)); + } + }, + error: function(jqXHR, status, error) { + dispatch(ErrorActions.ajaxError(error)); + } + }); + }; +} + +function remove(report) { + return function(dispatch) { + dispatch(removeReport()); + + $.ajax({ + type: "DELETE", + dataType: "json", + url: "report/"+report.ReportId+"/", + success: function(data, status, jqXHR) { + var e = new Error(); + e.fromJSON(data); + if (e.isError()) { + dispatch(ErrorActions.serverError(e)); + } else { + dispatch(reportRemoved(report.ReportId)); + } + }, + error: function(jqXHR, status, error) { + dispatch(ErrorActions.ajaxError(error)); + } + }); + }; +} + +function tabulate(report) { + return function (dispatch) { + dispatch(tabulateReport(report)); + + $.ajax({ + type: "GET", + dataType: "json", + url: "report/"+report.ReportId+"/tabulation/", + success: function(data, status, jqXHR) { + var e = new Error(); + e.fromJSON(data); + if (e.isError()) { + dispatch(ErrorActions.serverError(e)); + } else { + var t = new Tabulation(); + t.fromJSON(data); + dispatch(reportTabulated(report, t)); + } + }, + error: function(jqXHR, status, error) { + dispatch(ErrorActions.ajaxError(error)); + } + }); + }; +} + +function selectSeries(tabulation, seriesTraversal) { return function (dispatch) { if (!seriesTraversal) seriesTraversal = []; - dispatch(selectReport(report, seriesTraversal)); // Descend the tree to the right series to flatten - var series = report; + var series = tabulation; for (var i=0; i < seriesTraversal.length; i++) { if (!series.Series.hasOwnProperty(seriesTraversal[i])) { dispatch(ErrorActions.clientError("Invalid series")); @@ -87,23 +251,27 @@ function select(report, seriesTraversal) { // Add back in any values from the current level if (series.hasOwnProperty('Values')) - flattenedSeries[Report.topLevelAccountName()] = series.Values; + flattenedSeries[Tabulation.topLevelSeriesName()] = series.Values; - var flattenedReport = new Report(); + var flattenedTabulation = new Tabulation(); - flattenedReport.ReportId = report.ReportId; - flattenedReport.Title = report.Title; - flattenedReport.Subtitle = report.Subtitle; - flattenedReport.XAxisLabel = report.XAxisLabel; - flattenedReport.YAxisLabel = report.YAxisLabel; - flattenedReport.Labels = report.Labels.slice(); - flattenedReport.FlattenedSeries = flattenedSeries; + flattenedTabulation.ReportId = tabulation.ReportId; + flattenedTabulation.Title = tabulation.Title; + flattenedTabulation.Subtitle = tabulation.Subtitle; + flattenedTabulation.Units = tabulation.Units; + flattenedTabulation.Labels = tabulation.Labels.slice(); + flattenedTabulation.FlattenedSeries = flattenedSeries; - dispatch(reportSelected(flattenedReport, seriesTraversal)); + dispatch(seriesSelected(flattenedTabulation, seriesTraversal)); }; } module.exports = { - fetch: fetch, - select: select + fetchAll: fetchAll, + create: create, + update: update, + remove: remove, + tabulate: tabulate, + select: reportSelected, + selectSeries: selectSeries }; diff --git a/js/components/ReportsTab.js b/js/components/ReportsTab.js index 1e102eb..6771528 100644 --- a/js/components/ReportsTab.js +++ b/js/components/ReportsTab.js @@ -6,87 +6,101 @@ var Button = ReactBootstrap.Button; var Panel = ReactBootstrap.Panel; var StackedBarChart = require('../components/StackedBarChart'); +var PieChart = require('../components/PieChart'); var models = require('../models') var Report = models.Report; +var Tabulation = models.Tabulation; class ReportsTab extends React.Component { constructor() { super(); + this.state = { + initialized: false + } this.onSelectSeries = this.handleSelectSeries.bind(this); } componentWillMount() { - this.props.onFetchReport("monthly_expenses"); + this.props.onFetchAllReports(); } componentWillReceiveProps(nextProps) { - if (nextProps.reports['monthly_expenses'] && !nextProps.selectedReport.report) { - this.props.onSelectReport(nextProps.reports['monthly_expenses'], []); + var selected = nextProps.reports.selected; + if (!this.state.initialized) { + if (selected == -1 && + nextProps.reports.list.length > 0) + nextProps.onSelectReport(nextProps.reports.map[nextProps.reports.list[0]]); + this.setState({initialized: true}); + } else if (selected != -1 && !nextProps.reports.tabulations.hasOwnProperty(selected)) { + nextProps.onTabulateReport(nextProps.reports.map[nextProps.reports.list[0]]); + } else if (selected != -1 && nextProps.reports.selectedTabulation == null) { + nextProps.onSelectSeries(nextProps.reports.tabulations[nextProps.reports.list[0]]); } } handleSelectSeries(series) { - if (series == Report.topLevelAccountName()) + if (series == Tabulation.topLevelSeriesName()) return; - var seriesTraversal = this.props.selectedReport.seriesTraversal.slice(); + var seriesTraversal = this.props.selectedTabulation.seriesTraversal.slice(); seriesTraversal.push(series); - this.props.onSelectReport(this.props.reports[this.props.selectedReport.report.ReportId], seriesTraversal); + var selectedTabulation = this.props.reports.tabulations[this.props.reports.selected]; + this.props.onSelectSeries(selectedTabulation, seriesTraversal); } render() { - var report = []; - if (this.props.selectedReport.report) { - var titleTracks = []; - var seriesTraversal = []; - - for (var i = 0; i < this.props.selectedReport.seriesTraversal.length; i++) { - var name = this.props.selectedReport.report.Title; - if (i > 0) - name = this.props.selectedReport.seriesTraversal[i-1]; - - // Make a closure for going up the food chain - var self = this; - var navOnClick = function() { - var onSelectReport = self.props.onSelectReport; - var report = self.props.reports[self.props.selectedReport.report.ReportId]; - var mySeriesTraversal = seriesTraversal.slice(); - return function() { - onSelectReport(report, mySeriesTraversal); - }; - }(); - titleTracks.push(( - - )); - titleTracks.push((/)); - seriesTraversal.push(this.props.selectedReport.seriesTraversal[i]); - } - if (titleTracks.length == 0) { - titleTracks.push(( - - )); - } else { - var i = this.props.selectedReport.seriesTraversal.length-1; - titleTracks.push(( - - )); - } - - report = ( - - + var selectedTabulation = this.props.reports.selectedTabulation; + if (!selectedTabulation) { + return ( +
); } + + var titleTracks = []; + var seriesTraversal = []; + + for (var i = 0; i < this.props.selectedTabulation.seriesTraversal.length; i++) { + var name = this.props.selectedTabulation.tabulation.Title; + if (i > 0) + name = this.props.selectedTabulation.seriesTraversal[i-1]; + + // Make a closure for going up the food chain + var self = this; + var navOnClick = function() { + var onSelectTabulation = self.props.onSelectTabulation; + var report = self.props.reports[self.props.selectedTabulation.tabulation.ReportId]; + var mySeriesTraversal = seriesTraversal.slice(); + return function() { + onSelectTabulation(report, mySeriesTraversal); + }; + }(); + titleTracks.push(( + + )); + titleTracks.push((/)); + seriesTraversal.push(this.props.selectedTabulation.seriesTraversal[i]); + } + if (titleTracks.length == 0) { + titleTracks.push(( + + )); + } else { + var i = this.props.selectedTabulation.seriesTraversal.length-1; + titleTracks.push(( + + )); + } + return ( -
- {report} -
+ + + ); } } diff --git a/js/constants/ReportConstants.js b/js/constants/ReportConstants.js index da955c3..43a7164 100644 --- a/js/constants/ReportConstants.js +++ b/js/constants/ReportConstants.js @@ -1,8 +1,17 @@ var keyMirror = require('keymirror'); module.exports = keyMirror({ - FETCH_REPORT: null, - REPORT_FETCHED: null, - SELECT_REPORT: null, - REPORT_SELECTED: null + FETCH_REPORTS: null, + REPORTS_FETCHED: null, + CREATE_REPORT: null, + REPORT_CREATED: null, + UPDATE_REPORT: null, + REPORT_UPDATED: null, + REMOVE_REPORT: null, + REPORT_REMOVED: null, + TABULATE_REPORT: null, + REPORT_TABULATED: null, + REPORT_SELECTED: null, + SELECTION_CLEARED: null, + SERIES_SELECTED: null }); diff --git a/js/containers/ReportsTabContainer.js b/js/containers/ReportsTabContainer.js index d9bb3e6..f4df013 100644 --- a/js/containers/ReportsTabContainer.js +++ b/js/containers/ReportsTabContainer.js @@ -5,15 +5,19 @@ var ReportsTab = require('../components/ReportsTab'); function mapStateToProps(state) { return { - reports: state.reports, - selectedReport: state.selectedReport + reports: state.reports } } function mapDispatchToProps(dispatch) { return { - onFetchReport: function(reportname) {dispatch(ReportActions.fetch(reportname))}, - onSelectReport: function(report, seriesTraversal) {dispatch(ReportActions.select(report, seriesTraversal))} + onFetchAllReports: function() {dispatch(ReportActions.fetchAll())}, + onCreateReport: function(report) {dispatch(ReportActions.create(report))}, + onUpdateReport: function(report) {dispatch(ReportActions.update(report))}, + onDeleteReport: function(report) {dispatch(ReportActions.remove(report))}, + onSelectReport: function(report) {dispatch(ReportActions.select(report))}, + onTabulateReport: function(report) {dispatch(ReportActions.tabulate(report))}, + onSelectSeries: function(tabulation, seriesTraversal) {dispatch(ReportActions.selectSeries(tabulation, seriesTraversal))} } } diff --git a/js/models.js b/js/models.js index 504c7e2..3b5cae3 100644 --- a/js/models.js +++ b/js/models.js @@ -494,18 +494,17 @@ class Series { } } -class Report { +class Tabulation { constructor() { this.ReportId = ""; this.Title = ""; this.Subtitle = ""; - this.XAxisLabel = ""; - this.YAxisLabel = ""; + this.Units = ""; this.Labels = []; this.Series = {}; this.FlattenedSeries = {}; } - static topLevelAccountName() { + static topLevelSeriesName() { return "(top level)" } toJSON() { @@ -513,8 +512,7 @@ class Report { json_obj.ReportId = this.ReportId; json_obj.Title = this.Title; json_obj.Subtitle = this.Subtitle; - json_obj.XAxisLabel = this.XAxisLabel; - json_obj.YAxisLabel = this.YAxisLabel; + json_obj.Units = this.Units; json_obj.Labels = this.Labels; json_obj.Series = {}; for (var series in this.Series) { @@ -532,10 +530,8 @@ class Report { this.Title = json_obj.Title; if (json_obj.hasOwnProperty("Subtitle")) this.Subtitle = json_obj.Subtitle; - if (json_obj.hasOwnProperty("XAxisLabel")) - this.XAxisLabel = json_obj.XAxisLabel; - if (json_obj.hasOwnProperty("YAxisLabel")) - this.YAxisLabel = json_obj.YAxisLabel; + if (json_obj.hasOwnProperty("Units")) + this.Units = json_obj.Units; if (json_obj.hasOwnProperty("Labels")) this.Labels = json_obj.Labels; if (json_obj.hasOwnProperty("Series")) { @@ -582,7 +578,7 @@ module.exports = { Account: Account, Split: Split, Transaction: Transaction, - Report: Report, + Tabulation: Tabulation, OFXDownload: OFXDownload, Error: Error, diff --git a/js/reducers/MoneyGoReducer.js b/js/reducers/MoneyGoReducer.js index 456d665..3296906 100644 --- a/js/reducers/MoneyGoReducer.js +++ b/js/reducers/MoneyGoReducer.js @@ -8,7 +8,6 @@ var SecurityTemplateReducer = require('./SecurityTemplateReducer'); var SelectedAccountReducer = require('./SelectedAccountReducer'); var SelectedSecurityReducer = require('./SelectedSecurityReducer'); var ReportReducer = require('./ReportReducer'); -var SelectedReportReducer = require('./SelectedReportReducer'); var TransactionReducer = require('./TransactionReducer'); var TransactionPageReducer = require('./TransactionPageReducer'); var ImportReducer = require('./ImportReducer'); @@ -23,7 +22,6 @@ module.exports = Redux.combineReducers({ selectedAccount: SelectedAccountReducer, selectedSecurity: SelectedSecurityReducer, reports: ReportReducer, - selectedReport: SelectedReportReducer, transactions: TransactionReducer, transactionPage: TransactionPageReducer, imports: ImportReducer, diff --git a/js/reducers/ReportReducer.js b/js/reducers/ReportReducer.js index 7e44be4..ed5198a 100644 --- a/js/reducers/ReportReducer.js +++ b/js/reducers/ReportReducer.js @@ -3,15 +3,78 @@ var assign = require('object-assign'); var ReportConstants = require('../constants/ReportConstants'); var UserConstants = require('../constants/UserConstants'); -module.exports = function(state = {}, action) { +const initialState = { + map: {}, + tabulations: {}, + list: [], + selected: -1, + selectedTabulation: null, + seriesTraversal: [] +}; + +module.exports = function(state = initialState, action) { switch (action.type) { - case ReportConstants.REPORT_FETCHED: - var report = action.report; + case ReportConstants.REPORTS_FETCHED: + var selected = -1; + var reports = {}; + var list = []; + for (var i = 0; i < action.reports.length; i++) { + var report = action.reports[i]; + reports[report.ReportId] = report; + list.push(report.ReportId); + if (state.selected == report.ReportId) + selected = state.selected; + } return assign({}, state, { + map: reports, + list: list, + tabulations: {}, + selected: selected + }); + case ReportConstants.REPORT_CREATED: + case ReportConstants.REPORT_UPDATED: + var report = action.report; + var reports = assign({}, state.map, { [report.ReportId]: report }); + + var list = []; + for (var reportId in reports) { + if (reports.hasOwnProperty(reportId)) + list.push(report.ReportId); + } + return assign({}, state, { + map: reports, + list: list + }); + case ReportConstants.REPORT_REMOVED: + var selected = state.selected; + if (action.reportId == selected) + selected = -1; + var reports = assign({}, state.map); + delete reports[action.reportId]; + return assign({}, state, { + map: reports, + selected: selected + }); + case ReportConstants.REPORT_SELECTED: + return assign({}, state, { + selected: action.report.ReportId, + selectedTabulation: null, + seriesTraversal: [] + }); + case ReportConstants.TABULATION_FETCHED: + var tabulation = action.tabulation; + return assign({}, state, { + [tabulation.ReportId]: tabulation + }); + case ReportConstants.SERIES_SELECTED: + return { + selectedTabulation: action.tabulation, + seriesTraversal: action.seriesTraversal + }; case UserConstants.USER_LOGGEDOUT: - return {}; + return initialState; default: return state; } diff --git a/js/reducers/SelectedReportReducer.js b/js/reducers/SelectedReportReducer.js deleted file mode 100644 index 87ae68c..0000000 --- a/js/reducers/SelectedReportReducer.js +++ /dev/null @@ -1,23 +0,0 @@ -var assign = require('object-assign'); - -var ReportConstants = require('../constants/ReportConstants'); -var UserConstants = require('../constants/UserConstants'); - -const initialState = { - report: null, - seriesTraversal: [] -}; - -module.exports = function(state = initialState, action) { - switch (action.type) { - case ReportConstants.REPORT_SELECTED: - return { - report: action.report, - seriesTraversal: action.seriesTraversal - }; - case UserConstants.USER_LOGGEDOUT: - return initialState; - default: - return state; - } -}; diff --git a/reports.go b/reports.go index 81d72fa..0701381 100644 --- a/reports.go +++ b/reports.go @@ -4,14 +4,21 @@ import ( "context" "encoding/json" "errors" + "fmt" "github.com/yuin/gopher-lua" "log" "net/http" - "os" - "path" + "regexp" + "strings" "time" ) +var reportTabulationRE *regexp.Regexp + +func init() { + reportTabulationRE = regexp.MustCompile(`^/report/[0-9]+/tabulation/?$`) +} + //type and value to store user in lua's Context type key int @@ -24,19 +31,11 @@ const ( const luaTimeoutSeconds time.Duration = 30 // maximum time a lua request can run for -type Series struct { - Values []float64 - Series map[string]*Series -} - type Report struct { - ReportId string - Title string - Subtitle string - XAxisLabel string - YAxisLabel string - Labels []string - Series map[string]*Series + ReportId int64 + UserId int64 + Name string + Lua string } func (r *Report) Write(w http.ResponseWriter) error { @@ -44,7 +43,90 @@ func (r *Report) Write(w http.ResponseWriter) error { return enc.Encode(r) } -func runReport(user *User, reportpath string) (*Report, error) { +func (r *Report) Read(json_str string) error { + dec := json.NewDecoder(strings.NewReader(json_str)) + return dec.Decode(r) +} + +type ReportList struct { + Reports *[]Report `json:"reports"` +} + +func (rl *ReportList) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(rl) +} + +type Series struct { + Values []float64 + Series map[string]*Series +} + +type Tabulation struct { + ReportId int64 + Title string + Subtitle string + Units string + Labels []string + Series map[string]*Series +} + +func (r *Tabulation) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(r) +} + +func GetReport(reportid int64, userid int64) (*Report, error) { + var r Report + + err := DB.SelectOne(&r, "SELECT * from reports where UserId=? AND ReportId=?", userid, reportid) + if err != nil { + return nil, err + } + return &r, nil +} + +func GetReports(userid int64) (*[]Report, error) { + var reports []Report + + _, err := DB.Select(&reports, "SELECT * from reports where UserId=?", userid) + if err != nil { + return nil, err + } + return &reports, nil +} + +func InsertReport(r *Report) error { + err := DB.Insert(r) + if err != nil { + return err + } + return nil +} + +func UpdateReport(r *Report) error { + count, err := DB.Update(r) + if err != nil { + return err + } + if count != 1 { + return errors.New("Updated more than one report") + } + return nil +} + +func DeleteReport(r *Report) error { + count, err := DB.Delete(r) + if err != nil { + return err + } + if count != 1 { + return errors.New("Deleted more than one report") + } + return nil +} + +func runReport(user *User, report *Report) (*Tabulation, error) { // Create a new LState without opening the default libs for security L := lua.NewState(lua.Options{SkipOpenLibs: true}) defer L.Close() @@ -78,9 +160,9 @@ func runReport(user *User, reportpath string) (*Report, error) { luaRegisterSecurities(L) luaRegisterBalances(L) luaRegisterDates(L) - luaRegisterReports(L) + luaRegisterTabulations(L) - err := L.DoFile(reportpath) + err := L.DoString(report.Lua) if err != nil { return nil, err @@ -96,13 +178,36 @@ func runReport(user *User, reportpath string) (*Report, error) { value := L.Get(-1) if ud, ok := value.(*lua.LUserData); ok { - if report, ok := ud.Value.(*Report); ok { - return report, nil + if tabulation, ok := ud.Value.(*Tabulation); ok { + return tabulation, nil } else { - return nil, errors.New("generate() in " + reportpath + " didn't return a report") + return nil, fmt.Errorf("generate() for %s (Id: %d) didn't return a tabulation", report.Name, report.ReportId) } } else { - return nil, errors.New("generate() in " + reportpath + " didn't return a report") + return nil, fmt.Errorf("generate() for %s (Id: %d) didn't even return LUserData", report.Name, report.ReportId) + } +} + +func ReportTabulationHandler(w http.ResponseWriter, r *http.Request, user *User, reportid int64) { + report, err := GetReport(reportid, user.UserId) + if err != nil { + WriteError(w, 3 /*Invalid Request*/) + return + } + + tabulation, err := runReport(user, report) + if err != nil { + // TODO handle different failure cases differently + log.Print("runReport returned:", err) + WriteError(w, 3 /*Invalid Request*/) + return + } + + err = tabulation.Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return } } @@ -113,34 +218,132 @@ func ReportHandler(w http.ResponseWriter, r *http.Request) { return } - if r.Method == "GET" { - var reportname string - n, err := GetURLPieces(r.URL.Path, "/report/%s", &reportname) - if err != nil || n != 1 { + if r.Method == "POST" { + report_json := r.PostFormValue("report") + if report_json == "" { WriteError(w, 3 /*Invalid Request*/) return } - reportpath := path.Join(baseDir, "reports", reportname+".lua") - report_stat, err := os.Stat(reportpath) - if err != nil || !report_stat.Mode().IsRegular() { + var report Report + err := report.Read(report_json) + if err != nil { WriteError(w, 3 /*Invalid Request*/) return } + report.ReportId = -1 + report.UserId = user.UserId - report, err := runReport(user, reportpath) + err = InsertReport(&report) if err != nil { WriteError(w, 999 /*Internal Error*/) log.Print(err) return } - report.ReportId = reportname + w.WriteHeader(201 /*Created*/) err = report.Write(w) if err != nil { WriteError(w, 999 /*Internal Error*/) log.Print(err) return } + } else if r.Method == "GET" { + if reportTabulationRE.MatchString(r.URL.Path) { + var reportid int64 + n, err := GetURLPieces(r.URL.Path, "/report/%d/tabulation", &reportid) + if err != nil || n != 1 { + WriteError(w, 999 /*InternalError*/) + log.Print(err) + return + } + ReportTabulationHandler(w, r, user, reportid) + return + } + + var reportid int64 + n, err := GetURLPieces(r.URL.Path, "/report/%d", &reportid) + if err != nil || n != 1 { + //Return all Reports + var rl ReportList + reports, err := GetReports(user.UserId) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + rl.Reports = reports + err = (&rl).Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + } else { + // Return Report with this Id + report, err := GetReport(reportid, user.UserId) + if err != nil { + WriteError(w, 3 /*Invalid Request*/) + return + } + + err = report.Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + } + } else { + reportid, err := GetURLID(r.URL.Path) + if err != nil { + WriteError(w, 3 /*Invalid Request*/) + return + } + + if r.Method == "PUT" { + report_json := r.PostFormValue("report") + if report_json == "" { + WriteError(w, 3 /*Invalid Request*/) + return + } + + var report Report + err := report.Read(report_json) + if err != nil || report.ReportId != reportid { + WriteError(w, 3 /*Invalid Request*/) + return + } + report.UserId = user.UserId + + err = UpdateReport(&report) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + + err = report.Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + } else if r.Method == "DELETE" { + report, err := GetReport(reportid, user.UserId) + if err != nil { + WriteError(w, 3 /*Invalid Request*/) + return + } + + err = DeleteReport(report) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + + WriteSuccess(w) + } } } diff --git a/reports/monthly_expenses.lua b/reports/monthly_expenses.lua index 50b1968..9460c59 100644 --- a/reports/monthly_expenses.lua +++ b/reports/monthly_expenses.lua @@ -1,4 +1,4 @@ -function account_series_map(accounts, report) +function account_series_map(accounts, tabulation) map = {} for i=1,100 do -- we're not messing with accounts more than 100 levels deep @@ -7,7 +7,7 @@ function account_series_map(accounts, report) if not map[id] then all_handled = false if not acct.parent then - map[id] = report:series(acct.name) + map[id] = tabulation:series(acct.name) elseif map[acct.parent.accountid] then map[id] = map[acct.parent.accountid]:series(acct.name) end @@ -26,7 +26,7 @@ function generate() account_type = account.Expense accounts = get_accounts() - r = report.new(12) + r = tabulation.new(12) r:title(year .. " Monthly Expenses") series_map = account_series_map(accounts, r) diff --git a/reports/years_income.lua b/reports/years_income.lua index cd60373..408caab 100644 --- a/reports/years_income.lua +++ b/reports/years_income.lua @@ -1,4 +1,4 @@ -function account_series_map(accounts, report) +function account_series_map(accounts, tabulation) map = {} for i=1,100 do -- we're not messing with accounts more than 100 levels deep @@ -7,7 +7,7 @@ function account_series_map(accounts, report) if not map[id] then all_handled = false if not acct.parent then - map[id] = report:series(acct.name) + map[id] = tabulation:series(acct.name) elseif map[acct.parent.accountid] then map[id] = map[acct.parent.accountid]:series(acct.name) end @@ -26,7 +26,7 @@ function generate() account_type = account.Income accounts = get_accounts() - r = report.new(1) + r = tabulation.new(1) r:title(year .. " Income") series_map = account_series_map(accounts, r) diff --git a/reports_lua.go b/reports_lua.go index 9da36fe..a272393 100644 --- a/reports_lua.go +++ b/reports_lua.go @@ -4,14 +4,14 @@ import ( "github.com/yuin/gopher-lua" ) -const luaReportTypeName = "report" +const luaTabulationTypeName = "tabulation" const luaSeriesTypeName = "series" -func luaRegisterReports(L *lua.LState) { - mtr := L.NewTypeMetatable(luaReportTypeName) - L.SetGlobal("report", mtr) - L.SetField(mtr, "new", L.NewFunction(luaReportNew)) - L.SetField(mtr, "__index", L.NewFunction(luaReport__index)) +func luaRegisterTabulations(L *lua.LState) { + mtr := L.NewTypeMetatable(luaTabulationTypeName) + L.SetGlobal("tabulation", mtr) + L.SetField(mtr, "new", L.NewFunction(luaTabulationNew)) + L.SetField(mtr, "__index", L.NewFunction(luaTabulation__index)) L.SetField(mtr, "__metatable", lua.LString("protected")) mts := L.NewTypeMetatable(luaSeriesTypeName) @@ -20,13 +20,13 @@ func luaRegisterReports(L *lua.LState) { L.SetField(mts, "__metatable", lua.LString("protected")) } -// Checks whether the first lua argument is a *LUserData with *Report and returns *Report -func luaCheckReport(L *lua.LState, n int) *Report { +// Checks whether the first lua argument is a *LUserData with *Tabulation and returns *Tabulation +func luaCheckTabulation(L *lua.LState, n int) *Tabulation { ud := L.CheckUserData(n) - if report, ok := ud.Value.(*Report); ok { - return report + if tabulation, ok := ud.Value.(*Tabulation); ok { + return tabulation } - L.ArgError(n, "report expected") + L.ArgError(n, "tabulation expected") return nil } @@ -40,114 +40,101 @@ func luaCheckSeries(L *lua.LState, n int) *Series { return nil } -func luaReportNew(L *lua.LState) int { +func luaTabulationNew(L *lua.LState) int { numvalues := L.CheckInt(1) ud := L.NewUserData() - ud.Value = &Report{ + ud.Value = &Tabulation{ Labels: make([]string, numvalues), Series: make(map[string]*Series), } - L.SetMetatable(ud, L.GetTypeMetatable(luaReportTypeName)) + L.SetMetatable(ud, L.GetTypeMetatable(luaTabulationTypeName)) L.Push(ud) return 1 } -func luaReport__index(L *lua.LState) int { +func luaTabulation__index(L *lua.LState) int { field := L.CheckString(2) switch field { case "Label", "label": - L.Push(L.NewFunction(luaReportLabel)) + L.Push(L.NewFunction(luaTabulationLabel)) case "Series", "series": - L.Push(L.NewFunction(luaReportSeries)) + L.Push(L.NewFunction(luaTabulationSeries)) case "Title", "title": - L.Push(L.NewFunction(luaReportTitle)) + L.Push(L.NewFunction(luaTabulationTitle)) case "Subtitle", "subtitle": - L.Push(L.NewFunction(luaReportSubtitle)) - case "XAxisLabel", "xaxislabel": - L.Push(L.NewFunction(luaReportXAxis)) - case "YAxisLabel", "yaxislabel": - L.Push(L.NewFunction(luaReportYAxis)) + L.Push(L.NewFunction(luaTabulationSubtitle)) + case "Units", "units": + L.Push(L.NewFunction(luaTabulationUnits)) default: - L.ArgError(2, "unexpected report attribute: "+field) + L.ArgError(2, "unexpected tabulation attribute: "+field) } return 1 } -func luaReportLabel(L *lua.LState) int { - report := luaCheckReport(L, 1) +func luaTabulationLabel(L *lua.LState) int { + tabulation := luaCheckTabulation(L, 1) labelnumber := L.CheckInt(2) label := L.CheckString(3) - if labelnumber > cap(report.Labels) || labelnumber < 1 { + if labelnumber > cap(tabulation.Labels) || labelnumber < 1 { L.ArgError(2, "Label index must be between 1 and the number of data points, inclusive") } - report.Labels[labelnumber-1] = label + tabulation.Labels[labelnumber-1] = label return 0 } -func luaReportSeries(L *lua.LState) int { - report := luaCheckReport(L, 1) +func luaTabulationSeries(L *lua.LState) int { + tabulation := luaCheckTabulation(L, 1) name := L.CheckString(2) ud := L.NewUserData() - s, ok := report.Series[name] + s, ok := tabulation.Series[name] if ok { ud.Value = s } else { - report.Series[name] = &Series{ + tabulation.Series[name] = &Series{ Series: make(map[string]*Series), - Values: make([]float64, cap(report.Labels)), + Values: make([]float64, cap(tabulation.Labels)), } - ud.Value = report.Series[name] + ud.Value = tabulation.Series[name] } L.SetMetatable(ud, L.GetTypeMetatable(luaSeriesTypeName)) L.Push(ud) return 1 } -func luaReportTitle(L *lua.LState) int { - report := luaCheckReport(L, 1) +func luaTabulationTitle(L *lua.LState) int { + tabulation := luaCheckTabulation(L, 1) if L.GetTop() == 2 { - report.Title = L.CheckString(2) + tabulation.Title = L.CheckString(2) return 0 } - L.Push(lua.LString(report.Title)) + L.Push(lua.LString(tabulation.Title)) return 1 } -func luaReportSubtitle(L *lua.LState) int { - report := luaCheckReport(L, 1) +func luaTabulationSubtitle(L *lua.LState) int { + tabulation := luaCheckTabulation(L, 1) if L.GetTop() == 2 { - report.Subtitle = L.CheckString(2) + tabulation.Subtitle = L.CheckString(2) return 0 } - L.Push(lua.LString(report.Subtitle)) + L.Push(lua.LString(tabulation.Subtitle)) return 1 } -func luaReportXAxis(L *lua.LState) int { - report := luaCheckReport(L, 1) +func luaTabulationUnits(L *lua.LState) int { + tabulation := luaCheckTabulation(L, 1) if L.GetTop() == 2 { - report.XAxisLabel = L.CheckString(2) + tabulation.Units = L.CheckString(2) return 0 } - L.Push(lua.LString(report.XAxisLabel)) - return 1 -} - -func luaReportYAxis(L *lua.LState) int { - report := luaCheckReport(L, 1) - - if L.GetTop() == 2 { - report.YAxisLabel = L.CheckString(2) - return 0 - } - L.Push(lua.LString(report.YAxisLabel)) + L.Push(lua.LString(tabulation.Units)) return 1 }