diff --git a/js/actions/ReportActions.js b/js/actions/ReportActions.js new file mode 100644 index 0000000..d545d60 --- /dev/null +++ b/js/actions/ReportActions.js @@ -0,0 +1,50 @@ +var ReportConstants = require('../constants/ReportConstants'); + +var ErrorActions = require('./ErrorActions'); + +var Report = models.Report; +var Error = models.Error; + +function fetchReport(reportId) { + return { + type: ReportConstants.FETCH_REPORT, + reportId: reportId + } +} + +function reportFetched(report) { + return { + type: ReportConstants.REPORT_FETCHED, + report: report + } +} + +function fetch(reportId) { + return function (dispatch) { + dispatch(fetchReport(reportId)); + + $.ajax({ + type: "GET", + dataType: "json", + url: "report/?id="+reportId, + 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)); + } + }, + error: function(jqXHR, status, error) { + dispatch(ErrorActions.ajaxError(e)); + } + }); + }; +} + +module.exports = { + fetch: fetch +}; diff --git a/js/components/AttendeeFrequencyChart.js b/js/components/AttendeeFrequencyChart.js deleted file mode 100644 index 7d30d23..0000000 --- a/js/components/AttendeeFrequencyChart.js +++ /dev/null @@ -1,23 +0,0 @@ -var React = require('react'); - -var BarChart = require('../components/BarChart'); - -module.exports = React.createClass({ - displayName: "AttendeeFrequencyChart", - render: function() { - var data = []; - for (var i = 0; i < this.props.popularAttendees.length; i++) { - var attendee = this.props.popularAttendees[i]; - data.push({ - 'label': attendee.Name, - 'value': attendee.Popularity - }); - } - - data.sort(function(a, b){return b.value - a.value;}); - - return ( - - ); - } -}); diff --git a/js/components/BarChart.js b/js/components/BarChart.js index eaed00c..d7c28ca 100644 --- a/js/components/BarChart.js +++ b/js/components/BarChart.js @@ -7,17 +7,17 @@ module.exports = React.createClass({ render: function() { /* Expects 'this.props.data' to be in the form: * var data = [ - * {'label': 'foo', 'value': 1.4}, - * {'label': 'bar', 'value': 8} + * {'Label': 'foo', 'Value': 1.4}, + * {'Label': 'bar', 'Value': 8} * ]; */ if (this.props.data.length < 1) return (
); - var max = parseFloat(this.props.data[0].value); - var min = parseFloat(this.props.data[0].value); + var max = parseFloat(this.props.data[0].Value); + var min = parseFloat(this.props.data[0].Value); for (var i = 0; i < this.props.data.length; i++) { - var cur = parseFloat(this.props.data[i].value); + var cur = parseFloat(this.props.data[i].Value); if (cur > max) max = cur; if (cur < min) @@ -30,12 +30,12 @@ module.exports = React.createClass({ if ((max - min) == 0.0) var percent = 100; else if (min < 0) - var percent = 100*(parseFloat(rowData.value)-min)/(max-min); + var percent = 100*(parseFloat(rowData.Value)-min)/(max-min); else - var percent = 100*parseFloat(rowData.value)/max; + var percent = 100*parseFloat(rowData.Value)/max; rows.push(( - {rowData.label + " (" + rowData.value + ")"} + {rowData.Label + " (" + rowData.Value + ")"}
 
)); diff --git a/js/components/LunchApp.js b/js/components/LunchApp.js index f0acf6a..49d46dc 100644 --- a/js/components/LunchApp.js +++ b/js/components/LunchApp.js @@ -9,7 +9,7 @@ var Modal = ReactBootstrap.Modal; var TopBarContainer = require('../containers/TopBarContainer'); var RecordLunchContainer = require('../containers/RecordLunchContainer'); var AccountSettingsModalContainer = require('../containers/AccountSettingsModalContainer'); -var LunchStats = require('../components/LunchStats'); +var LunchStatsContainer = require('../containers/LunchStatsContainer'); var NewUserForm = require('./NewUserForm'); module.exports = React.createClass({ @@ -67,8 +67,8 @@ module.exports = React.createClass({ - - + + ); else diff --git a/js/components/LunchStats.js b/js/components/LunchStats.js index f235152..7e5e2dd 100644 --- a/js/components/LunchStats.js +++ b/js/components/LunchStats.js @@ -1,15 +1,50 @@ var React = require('react'); -var PopularSuggestionsContainer = require('../containers/PopularSuggestionsContainer'); -var AttendeeFrequencyContainer = require('../containers/AttendeeFrequencyContainer'); +var ReactBootstrap = require('react-bootstrap'); +var ButtonGroup = ReactBootstrap.ButtonGroup; +var DropdownButton = ReactBootstrap.DropdownButton; +var MenuItem = ReactBootstrap.MenuItem; + +var BarChart = require('../components/BarChart'); module.exports = React.createClass({ displayName: "LunchStats", + getInitialState: function() { + return { + selectedReportId: null + }; + }, + selectReport: function(reportId) { + this.props.fetchReport(reportId); + this.setState({ + selectedReportId: reportId + }); + }, render: function() { + var chart = (
Please select a report from above
); + if (this.state.selectedReportId && + this.props.reports.hasOwnProperty(this.state.selectedReportId)) { + var report = this.props.reports[this.state.selectedReportId]; + var data = report.Data; + data.sort(function(a, b){return b.Value - a.Value;}); + var chart=(); + } + return (
- - + + + Suggestion Frequency + Non-Vetoed Suggestions + Vetoed Suggestions + Attendee Frequency + + + + {chart}
); } diff --git a/js/components/PopularSuggestionsChart.js b/js/components/PopularSuggestionsChart.js deleted file mode 100644 index a84aaf1..0000000 --- a/js/components/PopularSuggestionsChart.js +++ /dev/null @@ -1,23 +0,0 @@ -var React = require('react'); - -var BarChart = require('../components/BarChart'); - -module.exports = React.createClass({ - displayName: "PopularSuggestionsChart", - render: function() { - var data = []; - for (var i = 0; i < this.props.popularSuggestions.length; i++) { - var suggestion = this.props.popularSuggestions[i]; - data.push({ - 'label': suggestion.RestaurantName, - 'value': suggestion.Popularity - }); - } - - data.sort(function(a, b){return b.value - a.value;}); - - return ( - - ); - } -}); diff --git a/js/constants/ReportConstants.js b/js/constants/ReportConstants.js new file mode 100644 index 0000000..7e8a913 --- /dev/null +++ b/js/constants/ReportConstants.js @@ -0,0 +1,6 @@ +var keyMirror = require('keymirror'); + +module.exports = keyMirror({ + FETCH_REPORT: null, + REPORT_FETCHED: null +}); diff --git a/js/containers/AttendeeFrequencyContainer.js b/js/containers/AttendeeFrequencyContainer.js deleted file mode 100644 index 514036a..0000000 --- a/js/containers/AttendeeFrequencyContainer.js +++ /dev/null @@ -1,18 +0,0 @@ -var connect = require('react-redux').connect; - -var AttendeeFrequencyChart = require('../components/AttendeeFrequencyChart'); - -function mapStateToProps(state) { - return { - popularAttendees: state.popularAttendees - } -} - -function mapDispatchToProps(dispatch) { - return {} -} - -module.exports = connect( - mapStateToProps, - mapDispatchToProps -)(AttendeeFrequencyChart) diff --git a/js/containers/LunchStatsContainer.js b/js/containers/LunchStatsContainer.js new file mode 100644 index 0000000..5b3755e --- /dev/null +++ b/js/containers/LunchStatsContainer.js @@ -0,0 +1,22 @@ +var connect = require('react-redux').connect; + +var LunchStats = require('../components/LunchStats'); + +var ReportActions = require('../actions/ReportActions'); + +function mapStateToProps(state) { + return { + reports: state.reports + } +} + +function mapDispatchToProps(dispatch) { + return { + fetchReport: function(reportId) {dispatch(ReportActions.fetch(reportId))}, + } +} + +module.exports = connect( + mapStateToProps, + mapDispatchToProps +)(LunchStats) diff --git a/js/containers/PopularSuggestionsContainer.js b/js/containers/PopularSuggestionsContainer.js deleted file mode 100644 index 8382ba3..0000000 --- a/js/containers/PopularSuggestionsContainer.js +++ /dev/null @@ -1,18 +0,0 @@ -var connect = require('react-redux').connect; - -var PopularSuggestionsChart = require('../components/PopularSuggestionsChart'); - -function mapStateToProps(state) { - return { - popularSuggestions: state.popularSuggestions - } -} - -function mapDispatchToProps(dispatch) { - return {} -} - -module.exports = connect( - mapStateToProps, - mapDispatchToProps -)(PopularSuggestionsChart) diff --git a/js/models.js b/js/models.js index 8ba2eb2..a65728e 100644 --- a/js/models.js +++ b/js/models.js @@ -218,6 +218,37 @@ Error.prototype.isError = function() { this.ErrorString != empty_error.ErrorString; } +function Report() { + this.ReportId = "invalid"; + this.Title = ""; + this.Data = []; +} + +Report.prototype.toJSON = function() { + var json_obj = {}; + json_obj.ReportId = this.ReportId; + json_obj.Title = this.Title; + json_obj.Data = this.Data; + return JSON.stringify(json_obj); +} + +Report.prototype.fromJSON = function(json_input) { + var json_obj = getJSONObj(json_input); + + if (json_obj.hasOwnProperty("ReportId")) + this.ReportId = json_obj.ReportId; + if (json_obj.hasOwnProperty("Title")) + this.Title = json_obj.Title; + if (json_obj.hasOwnProperty("Data")) + this.Data = json_obj.Data; +} + +Report.prototype.isReport = function() { + var empty_report = new Report(); + return this.ReportId != empty_report.ReportId || + this.Title != empty_report.Title; +} + module.exports = models = { // Classes @@ -228,6 +259,7 @@ module.exports = models = { PopularAttendee: PopularAttendee, Suggestion: Suggestion, PopularSuggestion: PopularSuggestion, + Report: Report, // Constants BogusPassword: "password" diff --git a/js/reducers/LunchReducer.js b/js/reducers/LunchReducer.js index 5195f73..cd42ac3 100644 --- a/js/reducers/LunchReducer.js +++ b/js/reducers/LunchReducer.js @@ -6,6 +6,7 @@ var AttendeeReducer = require('./AttendeeReducer'); var PopularAttendeeReducer = require('./PopularAttendeeReducer'); var SuggestionReducer = require('./SuggestionReducer'); var PopularSuggestionReducer = require('./PopularSuggestionReducer'); +var ReportReducer = require('./ReportReducer'); var ErrorReducer = require('./ErrorReducer'); module.exports = Redux.combineReducers({ @@ -15,5 +16,6 @@ module.exports = Redux.combineReducers({ popularAttendees: PopularAttendeeReducer, suggestions: SuggestionReducer, popularSuggestions: PopularSuggestionReducer, + reports: ReportReducer, error: ErrorReducer }); diff --git a/js/reducers/ReportReducer.js b/js/reducers/ReportReducer.js new file mode 100644 index 0000000..36277fa --- /dev/null +++ b/js/reducers/ReportReducer.js @@ -0,0 +1,19 @@ +var assign = require('object-assign'); + +var ReportConstants = require('../constants/ReportConstants'); +var UserConstants = require('../constants/UserConstants'); + +module.exports = function(state = {}, action) { + switch (action.type) { + case ReportConstants.REPORT_FETCHED: + var report = action.report; + var reports = assign({}, state, { + [report.ReportId]: report + }); + return reports; + case UserConstants.USER_LOGGEDOUT: + return {}; + default: + return state; + } +}; diff --git a/main.go b/main.go index b33a7a0..2bd58e8 100644 --- a/main.go +++ b/main.go @@ -69,6 +69,7 @@ func main() { servemux.HandleFunc("/popularattendees/", PopularAttendeeHandler) servemux.HandleFunc("/suggestion/", SuggestionHandler) servemux.HandleFunc("/popularsuggestions/", PopularSuggestionHandler) + servemux.HandleFunc("/report/", ReportHandler) listener, err := net.Listen("tcp", ":"+strconv.Itoa(port)) if err != nil { diff --git a/reports.go b/reports.go new file mode 100644 index 0000000..b85e501 --- /dev/null +++ b/reports.go @@ -0,0 +1,171 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "net/url" +) + +type ReportElement struct { + Label string + Value int64 +} + +type Report struct { + ReportId string + Title string + Data []*ReportElement +} + +func (r *ReportElement) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(r) +} + +func (r *Report) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(r) +} + +func (r *Report) FromSummedAttendeeList(attendees *[]*Attendee) { + attendeeMap := make(map[string]int64) + r.Data = make([]*ReportElement, 0) + + for i := range *attendees { + attendeeMap[(*attendees)[i].Name] += 1 + } + for name, count := range attendeeMap { + var element ReportElement + element.Label = name + element.Value = count + r.Data = append(r.Data, &element) + } +} + +func (r *Report) FromSummedSuggestionList(suggestions *[]*Suggestion) { + suggestionMap := make(map[string]int64) + r.Data = make([]*ReportElement, 0) + + for i := range *suggestions { + suggestionMap[(*suggestions)[i].RestaurantName] += 1 + } + for name, count := range suggestionMap { + var element ReportElement + element.Label = name + element.Value = count + r.Data = append(r.Data, &element) + } +} + +func GetAllSuggestions(userid int64) (*[]*Suggestion, error) { + var suggestions []*Suggestion + + _, err := DB.Select(&suggestions, "SELECT * from suggestions WHERE UserId=?", userid) + if err != nil { + return nil, err + } + + return &suggestions, nil +} + +func GetVetoedSuggestions(userid int64) (*[]*Suggestion, error) { + var suggestions []*Suggestion + + _, err := DB.Select(&suggestions, "SELECT suggestions.* FROM suggestions INNER JOIN suggestions AS s2 WHERE suggestions.UserId=? AND s2.UserId=? AND suggestions.Date=s2.Date AND s2.VetoingId=suggestions.SuggestionId", userid, userid) + if err != nil { + return nil, err + } + + return &suggestions, nil +} + +func GetNonVetoedSuggestions(userid int64) (*[]*Suggestion, error) { + var suggestions []*Suggestion + + _, err := DB.Select(&suggestions, "SELECT suggestions.* FROM suggestions LEFT OUTER JOIN suggestions AS s2 ON suggestions.SuggestionId=s2.VetoingId WHERE s2.SuggestionId IS NULL AND suggestions.UserId=?;", userid) + if err != nil { + return nil, err + } + + return &suggestions, nil +} + +func GetAllAttendees(userid int64) (*[]*Attendee, error) { + var attendees []*Attendee + + _, err := DB.Select(&attendees, "SELECT * from attendees WHERE UserId=?", userid) + if err != nil { + return nil, err + } + + return &attendees, nil +} + +func ReportHandler(w http.ResponseWriter, r *http.Request) { + user, err := GetUserFromSession(r) + if err != nil { + WriteError(w, 1 /*Not Signed In*/) + return + } + + if r.Method == "GET" { + var report Report + + query, _ := url.ParseQuery(r.URL.RawQuery) + reportid := query.Get("id") + report.ReportId = reportid + + if reportid == "non-vetoed-suggestions" { + report.Title = "Non-Vetoed Suggestions" + suggestions, err := GetNonVetoedSuggestions(user.UserId) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + report.FromSummedSuggestionList(suggestions) + } else if reportid == "vetoed-suggestions" { + report.Title = "Vetoed Suggestions" + suggestions, err := GetVetoedSuggestions(user.UserId) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + report.FromSummedSuggestionList(suggestions) + } else if reportid == "suggestions" { + report.Title = "Suggestion Frequency" + suggestions, err := GetAllSuggestions(user.UserId) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + report.FromSummedSuggestionList(suggestions) + } else if reportid == "attendees" { + report.Title = "Attendee Frequency" + attendees, err := GetAllAttendees(user.UserId) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + report.FromSummedAttendeeList(attendees) + } else { + WriteError(w, 3 /*Invalid Request*/) + return + } + + err = (&report).Write(w) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + } else { + /* No POST, PUT, or DELETE */ + WriteError(w, 3 /*Invalid Request*/) + return + } +} diff --git a/static/stylesheet.css b/static/stylesheet.css index 28bf107..0bc93a9 100644 --- a/static/stylesheet.css +++ b/static/stylesheet.css @@ -25,3 +25,7 @@ div.row div.panel div.panel-body { .addsuggestionbutton { margin-top: 25px; } + +.lunch-report-dropdown { + margin-bottom: 25px; +}