Add more reports

This commit is contained in:
Aaron Lindsay 2017-01-08 09:47:30 -05:00
parent 908e10736a
commit 4e547fe4fd
16 changed files with 357 additions and 97 deletions

View File

@ -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
};

View File

@ -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 (
<BarChart title="Attendee Frequency" data={data}/>
);
}
});

View File

@ -7,17 +7,17 @@ module.exports = React.createClass({
render: function() { render: function() {
/* Expects 'this.props.data' to be in the form: /* Expects 'this.props.data' to be in the form:
* var data = [ * var data = [
* {'label': 'foo', 'value': 1.4}, * {'Label': 'foo', 'Value': 1.4},
* {'label': 'bar', 'value': 8} * {'Label': 'bar', 'Value': 8}
* ]; * ];
*/ */
if (this.props.data.length < 1) if (this.props.data.length < 1)
return (<div />); return (<div />);
var max = parseFloat(this.props.data[0].value); var max = parseFloat(this.props.data[0].Value);
var min = parseFloat(this.props.data[0].value); var min = parseFloat(this.props.data[0].Value);
for (var i = 0; i < this.props.data.length; i++) { 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) if (cur > max)
max = cur; max = cur;
if (cur < min) if (cur < min)
@ -30,12 +30,12 @@ module.exports = React.createClass({
if ((max - min) == 0.0) if ((max - min) == 0.0)
var percent = 100; var percent = 100;
else if (min < 0) else if (min < 0)
var percent = 100*(parseFloat(rowData.value)-min)/(max-min); var percent = 100*(parseFloat(rowData.Value)-min)/(max-min);
else else
var percent = 100*parseFloat(rowData.value)/max; var percent = 100*parseFloat(rowData.Value)/max;
rows.push(( rows.push((
<tr key={i}> <tr key={i}>
<td style={{'width': '20%'}}>{rowData.label + " (" + rowData.value + ")"}</td> <td style={{'width': '20%'}}>{rowData.Label + " (" + rowData.Value + ")"}</td>
<td style={{'width': '80%'}}><div style={{'width': percent + "%", 'backgroundColor': 'steelblue'}}>&nbsp;</div></td> <td style={{'width': '80%'}}><div style={{'width': percent + "%", 'backgroundColor': 'steelblue'}}>&nbsp;</div></td>
</tr> </tr>
)); ));

View File

@ -9,7 +9,7 @@ var Modal = ReactBootstrap.Modal;
var TopBarContainer = require('../containers/TopBarContainer'); var TopBarContainer = require('../containers/TopBarContainer');
var RecordLunchContainer = require('../containers/RecordLunchContainer'); var RecordLunchContainer = require('../containers/RecordLunchContainer');
var AccountSettingsModalContainer = require('../containers/AccountSettingsModalContainer'); var AccountSettingsModalContainer = require('../containers/AccountSettingsModalContainer');
var LunchStats = require('../components/LunchStats'); var LunchStatsContainer = require('../containers/LunchStatsContainer');
var NewUserForm = require('./NewUserForm'); var NewUserForm = require('./NewUserForm');
module.exports = React.createClass({ module.exports = React.createClass({
@ -67,8 +67,8 @@ module.exports = React.createClass({
<Tab title="Record Lunch" eventKey={1} > <Tab title="Record Lunch" eventKey={1} >
<RecordLunchContainer /> <RecordLunchContainer />
</Tab> </Tab>
<Tab title="Statistics" eventKey={2} > <Tab title="Reports" eventKey={2} >
<LunchStats /> <LunchStatsContainer />
</Tab> </Tab>
</Tabs>); </Tabs>);
else else

View File

@ -1,15 +1,50 @@
var React = require('react'); var React = require('react');
var PopularSuggestionsContainer = require('../containers/PopularSuggestionsContainer'); var ReactBootstrap = require('react-bootstrap');
var AttendeeFrequencyContainer = require('../containers/AttendeeFrequencyContainer'); var ButtonGroup = ReactBootstrap.ButtonGroup;
var DropdownButton = ReactBootstrap.DropdownButton;
var MenuItem = ReactBootstrap.MenuItem;
var BarChart = require('../components/BarChart');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: "LunchStats", displayName: "LunchStats",
getInitialState: function() {
return {
selectedReportId: null
};
},
selectReport: function(reportId) {
this.props.fetchReport(reportId);
this.setState({
selectedReportId: reportId
});
},
render: function() { render: function() {
var chart = (<div>Please select a report from above</div>);
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=(<BarChart title={report.Title} data={data}/>);
}
return ( return (
<div> <div>
<AttendeeFrequencyContainer /> <ButtonGroup bsClass="lunch-report-dropdown">
<PopularSuggestionsContainer /> <DropdownButton
title="Select Report"
id="lunch-report-selection-dropdown"
onSelect={this.selectReport}>
<MenuItem eventKey="suggestions">Suggestion Frequency</MenuItem>
<MenuItem eventKey="non-vetoed-suggestions">Non-Vetoed Suggestions</MenuItem>
<MenuItem eventKey="vetoed-suggestions">Vetoed Suggestions</MenuItem>
<MenuItem eventKey="attendees">Attendee Frequency</MenuItem>
</DropdownButton>
</ButtonGroup>
{chart}
</div> </div>
); );
} }

View File

@ -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 (
<BarChart title="Suggestion Frequency" data={data}/>
);
}
});

View File

@ -0,0 +1,6 @@
var keyMirror = require('keymirror');
module.exports = keyMirror({
FETCH_REPORT: null,
REPORT_FETCHED: null
});

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -218,6 +218,37 @@ Error.prototype.isError = function() {
this.ErrorString != empty_error.ErrorString; 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 = { module.exports = models = {
// Classes // Classes
@ -228,6 +259,7 @@ module.exports = models = {
PopularAttendee: PopularAttendee, PopularAttendee: PopularAttendee,
Suggestion: Suggestion, Suggestion: Suggestion,
PopularSuggestion: PopularSuggestion, PopularSuggestion: PopularSuggestion,
Report: Report,
// Constants // Constants
BogusPassword: "password" BogusPassword: "password"

View File

@ -6,6 +6,7 @@ var AttendeeReducer = require('./AttendeeReducer');
var PopularAttendeeReducer = require('./PopularAttendeeReducer'); var PopularAttendeeReducer = require('./PopularAttendeeReducer');
var SuggestionReducer = require('./SuggestionReducer'); var SuggestionReducer = require('./SuggestionReducer');
var PopularSuggestionReducer = require('./PopularSuggestionReducer'); var PopularSuggestionReducer = require('./PopularSuggestionReducer');
var ReportReducer = require('./ReportReducer');
var ErrorReducer = require('./ErrorReducer'); var ErrorReducer = require('./ErrorReducer');
module.exports = Redux.combineReducers({ module.exports = Redux.combineReducers({
@ -15,5 +16,6 @@ module.exports = Redux.combineReducers({
popularAttendees: PopularAttendeeReducer, popularAttendees: PopularAttendeeReducer,
suggestions: SuggestionReducer, suggestions: SuggestionReducer,
popularSuggestions: PopularSuggestionReducer, popularSuggestions: PopularSuggestionReducer,
reports: ReportReducer,
error: ErrorReducer error: ErrorReducer
}); });

View File

@ -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;
}
};

View File

@ -69,6 +69,7 @@ func main() {
servemux.HandleFunc("/popularattendees/", PopularAttendeeHandler) servemux.HandleFunc("/popularattendees/", PopularAttendeeHandler)
servemux.HandleFunc("/suggestion/", SuggestionHandler) servemux.HandleFunc("/suggestion/", SuggestionHandler)
servemux.HandleFunc("/popularsuggestions/", PopularSuggestionHandler) servemux.HandleFunc("/popularsuggestions/", PopularSuggestionHandler)
servemux.HandleFunc("/report/", ReportHandler)
listener, err := net.Listen("tcp", ":"+strconv.Itoa(port)) listener, err := net.Listen("tcp", ":"+strconv.Itoa(port))
if err != nil { if err != nil {

171
reports.go Normal file
View File

@ -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
}
}

View File

@ -25,3 +25,7 @@ div.row div.panel div.panel-body {
.addsuggestionbutton { .addsuggestionbutton {
margin-top: 25px; margin-top: 25px;
} }
.lunch-report-dropdown {
margin-bottom: 25px;
}