diff --git a/attendees.go b/attendees.go index 715c563..8b599c0 100644 --- a/attendees.go +++ b/attendees.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "errors" "log" "net/http" "strings" @@ -71,7 +72,7 @@ func GetAttendees(userid int64, date time.Time) (*[]*Attendee, error) { func GetPopularAttendees() (*[]*PopularAttendee, error) { var attendees []*Attendee - var attendeeMap map[string]int64 + attendeeMap := make(map[string]int64) popularAttendees := make([]*PopularAttendee, 0) _, err := DB.Select(&attendees, "SELECT * from attendees") @@ -98,7 +99,7 @@ func InsertAttendee(a *Attendee) error { return err } - existing, err := transaction.SelectInt("SELECT count(*) from users where Name=?", a.Name) + existing, err := transaction.SelectInt("SELECT count(*) from attendees where UserId=? AND Name=? AND Date=?", a.UserId, a.Name, a.Date) if err != nil { transaction.Rollback() return err @@ -123,6 +124,41 @@ func InsertAttendee(a *Attendee) error { return nil } +func GetAttendee(attendeeid int64, userid int64, date time.Time) (*Attendee, error) { + var a Attendee + + err := DB.SelectOne(&a, "SELECT * from attendees where UserId=? AND AttendeeId=? AND Date=?", userid, attendeeid, date) + if err != nil { + return nil, err + } + return &a, nil +} + +func DeleteAttendee(a *Attendee) error { + transaction, err := DB.Begin() + if err != nil { + return err + } + + count, err := transaction.Delete(a) + if err != nil { + transaction.Rollback() + return err + } + if count != 1 { + transaction.Rollback() + return errors.New("Was going to delete more than one attendee") + } + + err = transaction.Commit() + if err != nil { + transaction.Rollback() + return err + } + + return nil +} + func AttendeeHandler(w http.ResponseWriter, r *http.Request) { user, err := GetUserFromSession(r) if err != nil { @@ -184,8 +220,29 @@ func AttendeeHandler(w http.ResponseWriter, r *http.Request) { log.Print(err) return } + } else if r.Method == "DELETE" { + attendeeid, err := GetURLID(r.URL.Path) + if err != nil { + WriteError(w, 3 /* Invalid Request */) + return + } + + attendee, err := GetAttendee(attendeeid, user.UserId, today) + if err != nil { + WriteError(w, 3 /*Invalid Request*/) + return + } + + err = DeleteAttendee(attendee) + if err != nil { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + return + } + + WriteSuccess(w) } else { - /* No PUT or DELETE */ + /* No PUT */ WriteError(w, 3 /*Invalid Request*/) return } diff --git a/js/actions/AttendeeActions.js b/js/actions/AttendeeActions.js index a17bd8a..0f2e729 100644 --- a/js/actions/AttendeeActions.js +++ b/js/actions/AttendeeActions.js @@ -2,8 +2,8 @@ var AttendeeConstants = require('../constants/AttendeeConstants'); var ErrorActions = require('./ErrorActions'); -var models = require('../models.js'); var Attendee = models.Attendee; +var PopularAttendee = models.PopularAttendee; var Error = models.Error; function fetchAttendees() { @@ -32,6 +32,19 @@ function attendeeCreated(attendee) { } } +function removeAttendee() { + return { + type: AttendeeConstants.REMOVE_ATTENDEE + } +} + +function attendeeRemoved(attendeeId) { + return { + type: AttendeeConstants.ATTENDEE_REMOVED, + attendeeId: attendeeId + } +} + function fetchPopularAttendees() { return { type: AttendeeConstants.FETCH_POPULAR_ATTENDEES @@ -114,8 +127,8 @@ function fetchPopular() { if (e.isError()) { ErrorActions.serverError(e); } else { - dispatch(popularAttendeesFetched(data.attendees.map(function(json) { - var a = new Attendee(); + dispatch(popularAttendeesFetched(data.popularattendees.map(function(json) { + var a = new PopularAttendee(); a.fromJSON(json); return a; }))); @@ -128,8 +141,33 @@ function fetchPopular() { }; } +function remove(attendee) { + return function(dispatch) { + dispatch(removeAttendee()); + + $.ajax({ + type: "DELETE", + dataType: "json", + url: "attendee/"+attendee.AttendeeId+"/", + success: function(data, status, jqXHR) { + var e = new Error(); + e.fromJSON(data); + if (e.isError()) { + ErrorActions.serverError(e); + } else { + dispatch(attendeeRemoved(attendee.AttendeeId)); + } + }, + error: function(jqXHR, status, error) { + ErrorActions.ajaxError(e); + } + }); + }; +} + module.exports = { fetchAll: fetchAll, create: create, + remove: remove, fetchPopular: fetchPopular }; diff --git a/js/actions/ErrorActions.js b/js/actions/ErrorActions.js index 38479b4..f34862a 100644 --- a/js/actions/ErrorActions.js +++ b/js/actions/ErrorActions.js @@ -1,6 +1,6 @@ var ErrorConstants = require('../constants/ErrorConstants'); -var models = require('../models.js'); +var models = require('../models'); var Error = models.Error; function serverError(error) { diff --git a/js/actions/SuggestionActions.js b/js/actions/SuggestionActions.js index c67cc51..b8ac810 100644 --- a/js/actions/SuggestionActions.js +++ b/js/actions/SuggestionActions.js @@ -2,8 +2,9 @@ var SuggestionConstants = require('../constants/SuggestionConstants'); var ErrorActions = require('./ErrorActions'); -var models = require('../models.js'); +var models = require('../models'); var Suggestion = models.Suggestion; +var PopularSuggestion = models.PopularSuggestion; var Error = models.Error; function fetchSuggestions() { @@ -114,8 +115,8 @@ function fetchPopular() { if (e.isError()) { ErrorActions.serverError(e); } else { - dispatch(popularSuggestionsFetched(data.suggestions.map(function(json) { - var a = new Suggestion(); + dispatch(popularSuggestionsFetched(data.popularsuggestions.map(function(json) { + var a = new PopularSuggestion(); a.fromJSON(json); return a; }))); diff --git a/js/actions/UserActions.js b/js/actions/UserActions.js index 48bfde2..eb16863 100644 --- a/js/actions/UserActions.js +++ b/js/actions/UserActions.js @@ -4,7 +4,7 @@ var AttendeeActions = require('./AttendeeActions'); var SuggestionActions = require('./SuggestionActions'); var ErrorActions = require('./ErrorActions'); -var models = require('../models.js'); +var models = require('../models'); var User = models.User; var Session = models.Session; var Error = models.Error; diff --git a/js/components/LunchApp.js b/js/components/LunchApp.js index 3ca8289..29e5ca5 100644 --- a/js/components/LunchApp.js +++ b/js/components/LunchApp.js @@ -7,8 +7,9 @@ var Tab = ReactBootstrap.Tab; var Modal = ReactBootstrap.Modal; var TopBarContainer = require('../containers/TopBarContainer'); -var NewUserForm = require('./NewUserForm'); +var RecordLunchContainer = require('../containers/RecordLunchContainer'); var AccountSettingsModalContainer = require('../containers/AccountSettingsModalContainer'); +var NewUserForm = require('./NewUserForm'); module.exports = React.createClass({ displayName: "LunchApp", @@ -62,7 +63,8 @@ module.exports = React.createClass({ if (this.props.user.isUser()) mainContent = ( - accounts + + stats will go here diff --git a/js/components/RecordLunch.js b/js/components/RecordLunch.js new file mode 100644 index 0000000..3a0f21f --- /dev/null +++ b/js/components/RecordLunch.js @@ -0,0 +1,71 @@ +var React = require('react'); + +var ReactBootstrap = require('react-bootstrap'); +var FormGroup = ReactBootstrap.FormGroup; +var ControlLabel = ReactBootstrap.ControlLabel; + +var Multiselect = require('react-widgets').Multiselect; + +var models = require('../models') +var Attendee = models.Attendee + +module.exports = React.createClass({ + displayName: "RecordLunch", + getInitialState: function() { + return { + }; + }, + getAttendeeList: function() { + var attendeeList = []; + for (var attendeeId in this.props.attendees) { + attendeeList.push(this.props.attendees[attendeeId]); + } + return attendeeList; + }, + getAttendeeMap: function() { + var attendeeMap = {}; + for (var attendeeId in this.props.attendees) { + var attendee = this.props.attendees[attendeeId]; + attendeeMap[attendee.Name] = attendee; + } + return attendeeMap; + }, + onChangeAttendees: function(attendees) { + var attendeeMap = this.getAttendeeMap(); + for (var i in attendees) { + if (attendeeMap.hasOwnProperty(attendees[i].Name)) { + delete attendeeMap[attendees[i].Name]; + } else { + var attendee = new Attendee(); + attendee.Name = attendees[i].Name; + this.props.createAttendee(attendee); + } + } + for (var i in attendeeMap) { + this.props.removeAttendee(attendeeMap[i]); + } + }, + onCreateAttendee: function(attendeeName) { + var attendee = new Attendee(); + attendee.Name = attendeeName; + this.props.createAttendee(attendee); + }, + render: function() { + var attendeeList = this.getAttendeeList(); + return ( +
+ + Attendees + + +
+ ); + } +}); diff --git a/js/constants/AttendeeConstants.js b/js/constants/AttendeeConstants.js index 6981835..262f2a6 100644 --- a/js/constants/AttendeeConstants.js +++ b/js/constants/AttendeeConstants.js @@ -5,6 +5,8 @@ module.exports = keyMirror({ ATTENDEES_FETCHED: null, CREATE_ATTENDEE: null, ATTENDEE_CREATED: null, + REMOVE_ATTENDEE: null, + ATTENDEE_REMOVED: null, FETCH_POPULAR_ATTENDEES: null, POPULAR_ATTENDEES_FETCHED: null }); diff --git a/js/containers/RecordLunchContainer.js b/js/containers/RecordLunchContainer.js new file mode 100644 index 0000000..755ff39 --- /dev/null +++ b/js/containers/RecordLunchContainer.js @@ -0,0 +1,29 @@ +var connect = require('react-redux').connect; + +var UserActions = require('../actions/UserActions'); +var AttendeeActions = require('../actions/AttendeeActions'); +var SuggestionActions = require('../actions/SuggestionActions'); + +var RecordLunch = require('../components/RecordLunch'); + +function mapStateToProps(state) { + return { + attendees: state.attendees, + popularAttendees: state.popularAttendees, + suggestions: state.suggestions, + popularSuggestions: state.popularSuggestions + } +} + +function mapDispatchToProps(dispatch) { + return { + createAttendee: function(attendee) {dispatch(AttendeeActions.create(attendee))}, + removeAttendee: function(attendee) {dispatch(AttendeeActions.remove(attendee))}, + createSuggestion: function(suggestion) {dispatch(SuggestionActions.create(suggestion))} + } +} + +module.exports = connect( + mapStateToProps, + mapDispatchToProps +)(RecordLunch) diff --git a/js/models.js b/js/models.js index 387a9e7..ae5e21b 100644 --- a/js/models.js +++ b/js/models.js @@ -101,6 +101,33 @@ Attendee.prototype.isAttendee = function() { this.Name != empty_attendee.Name; } +function PopularAttendee() { + this.Name = ""; + this.Popularity = 0; +} + +PopularAttendee.prototype.toJSON = function() { + var json_obj = {}; + json_obj.Name = this.Name; + json_obj.Popularity = this.Popularity; + return JSON.stringify(json_obj); +} + +PopularAttendee.prototype.fromJSON = function(json_input) { + var json_obj = getJSONObj(json_input); + + if (json_obj.hasOwnProperty("Popularity")) + this.Popularity = json_obj.Popularity; + if (json_obj.hasOwnProperty("Name")) + this.Name = json_obj.Name; +} + +PopularAttendee.prototype.isPopularAttendee = function() { + var empty_attendee = new PopularAttendee(); + return this.Popularity != empty_attendee.Popularity || + this.Name != empty_attendee.Name; +} + function Suggestion() { this.SuggestionId = -1; this.VetoingId = -1; @@ -134,6 +161,33 @@ Suggestion.prototype.isSuggestion = function() { this.RestaurantName != empty_suggestion.RestaurantName; } +function PopularSuggestion() { + this.RestaurantName = ""; + this.Popularity = 0; +} + +PopularSuggestion.prototype.toJSON = function() { + var json_obj = {}; + json_obj.RestaurantName = this.RestaurantName; + json_obj.Popularity = this.Popularity; + return JSON.stringify(json_obj); +} + +PopularSuggestion.prototype.fromJSON = function(json_input) { + var json_obj = getJSONObj(json_input); + + if (json_obj.hasOwnProperty("RestaurantName")) + this.RestaurantName = json_obj.RestaurantName; + if (json_obj.hasOwnProperty("Popularity")) + this.Popularity = json_obj.Popularity; +} + +PopularSuggestion.prototype.isPopularSuggestion = function() { + var empty_suggestion = new PopularSuggestion(); + return this.RestaurantName != empty_suggestion.RestaurantName && + this.Popularity != empty_suggestion.Popularity; +} + function Error() { this.ErrorId = -1; this.ErrorString = ""; @@ -167,6 +221,10 @@ module.exports = models = { User: User, Session: Session, Error: Error, + Attendee: Attendee, + PopularAttendee: PopularAttendee, + Suggestion: Suggestion, + PopularSuggestion: PopularSuggestion, // Constants BogusPassword: "password" diff --git a/js/reducers/AttendeeReducer.js b/js/reducers/AttendeeReducer.js index 63ffe57..cefe50f 100644 --- a/js/reducers/AttendeeReducer.js +++ b/js/reducers/AttendeeReducer.js @@ -18,6 +18,10 @@ module.exports = function(state = {}, action) { [attendee.AttendeeId]: attendee }); return attendees; + case AttendeeConstants.ATTENDEE_REMOVED: + var attendees = assign({}, state); + delete attendees[action.attendeeId]; + return attendees; case UserConstants.USER_LOGGEDOUT: return {}; default: diff --git a/js/reducers/PopularAttendeeReducer.js b/js/reducers/PopularAttendeeReducer.js index 86ca11f..de993e4 100644 --- a/js/reducers/PopularAttendeeReducer.js +++ b/js/reducers/PopularAttendeeReducer.js @@ -3,17 +3,17 @@ var assign = require('object-assign'); var AttendeeConstants = require('../constants/AttendeeConstants'); var UserConstants = require('../constants/UserConstants'); -module.exports = function(state = {}, action) { +module.exports = function(state = [], action) { switch (action.type) { case AttendeeConstants.POPULAR_ATTENDEES_FETCHED: - var attendees = {}; + var attendees = []; for (var i = 0; i < action.attendees.length; i++) { - var attendee = action.attendees[i]; - attendees[attendee.AttendeeId] = attendee; + attendees.push(action.attendees[i]); } + attendees.sort(function(a, b){return a.Popularity - b.Popularity}); return attendees; case UserConstants.USER_LOGGEDOUT: - return {}; + return []; default: return state; } diff --git a/js/reducers/PopularSuggestionReducer.js b/js/reducers/PopularSuggestionReducer.js index 08b9580..d2b2b79 100644 --- a/js/reducers/PopularSuggestionReducer.js +++ b/js/reducers/PopularSuggestionReducer.js @@ -3,17 +3,17 @@ var assign = require('object-assign'); var SuggestionConstants = require('../constants/SuggestionConstants'); var UserConstants = require('../constants/UserConstants'); -module.exports = function(state = {}, action) { +module.exports = function(state = [], action) { switch (action.type) { case SuggestionConstants.POPULAR_SUGGESTIONS_FETCHED: - var suggestions = {}; + var suggestions = []; for (var i = 0; i < action.suggestions.length; i++) { - var suggestion = action.suggestions[i]; - suggestions[suggestion.SuggestionId] = suggestion; + suggestions.push(action.suggestions[i]); } + suggestions.sort(function(a, b){return a.Popularity - b.Popularity}); return suggestions; case UserConstants.USER_LOGGEDOUT: - return {}; + return []; default: return state; } diff --git a/static/stylesheet.css b/static/stylesheet.css index d8ac3e6..b9b9d40 100644 --- a/static/stylesheet.css +++ b/static/stylesheet.css @@ -5,12 +5,14 @@ div#content { display: block; width: 95%; height: 100%; - min-width: 75em; + min-width: 20em; max-width: 100em; margin: auto; } -/* Keep the main windows sized to the full viewable height */ -.fullheight { - height: 100%; +div.tab-content { + padding: 1em; + border-bottom: 1px solid #ddd; + border-left: 1px solid #ddd; + border-right: 1px solid #ddd; } diff --git a/suggestions.go b/suggestions.go index 352a73f..77d1262 100644 --- a/suggestions.go +++ b/suggestions.go @@ -73,7 +73,7 @@ func GetSuggestions(userid int64, date time.Time) (*[]*Suggestion, error) { func GetPopularSuggestions() (*[]*PopularSuggestion, error) { var suggestions []*Suggestion - var suggestionMap map[string]int64 + suggestionMap := make(map[string]int64) popularSuggestions := make([]*PopularSuggestion, 0) _, err := DB.Select(&suggestions, "SELECT * from suggestions")