diff --git a/errors.go b/errors.go index 0af9b8a..ae7c35a 100644 --- a/errors.go +++ b/errors.go @@ -12,14 +12,14 @@ type Error struct { } var error_codes = map[int]string{ - 1: "Not Signed In", - 2: "Unauthorized Access", - 3: "Invalid Request", - 4: "User Exists", - 5: "Attendee Exists", - 6: "Suggestion Exists", - 7: "Attendee In Use", - // 5: "Connection Failed", //client-side error + 1: "Not Signed In", + 2: "Unauthorized Access", + 3: "Invalid Request", + 4: "User Exists", + 5: "Attendee Exists", + 6: "Suggestion Exists", + 7: "Attendee In Use", + 8: "Suggestion Not Last", 999: "Internal Error", } diff --git a/js/actions/SuggestionActions.js b/js/actions/SuggestionActions.js index b8ac810..ab2b656 100644 --- a/js/actions/SuggestionActions.js +++ b/js/actions/SuggestionActions.js @@ -33,6 +33,19 @@ function suggestionCreated(suggestion) { } } +function removeSuggestion() { + return { + type: SuggestionConstants.REMOVE_SUGGESTION + } +} + +function suggestionRemoved(suggestionId) { + return { + type: SuggestionConstants.SUGGESTION_REMOVED, + suggestionId: suggestionId + } +} + function fetchPopularSuggestions() { return { type: SuggestionConstants.FETCH_POPULAR_SUGGESTIONS @@ -129,8 +142,33 @@ function fetchPopular() { }; } +function remove(suggestion) { + return function(dispatch) { + dispatch(removeSuggestion()); + + $.ajax({ + type: "DELETE", + dataType: "json", + url: "suggestion/"+suggestion.SuggestionId+"/", + success: function(data, status, jqXHR) { + var e = new Error(); + e.fromJSON(data); + if (e.isError()) { + ErrorActions.serverError(e); + } else { + dispatch(suggestionRemoved(suggestion.SuggestionId)); + } + }, + error: function(jqXHR, status, error) { + ErrorActions.ajaxError(e); + } + }); + }; +} + module.exports = { fetchAll: fetchAll, create: create, + remove: remove, fetchPopular: fetchPopular }; diff --git a/js/components/NewSuggestion.js b/js/components/NewSuggestion.js index c9b8ddf..b4a37f6 100644 --- a/js/components/NewSuggestion.js +++ b/js/components/NewSuggestion.js @@ -4,6 +4,7 @@ var ReactBootstrap = require('react-bootstrap'); var Grid = ReactBootstrap.Grid; var Row = ReactBootstrap.Row; var Col = ReactBootstrap.Col; +var FormGroup = ReactBootstrap.FormGroup; var ControlLabel = ReactBootstrap.ControlLabel; var Button = ReactBootstrap.Button; @@ -102,31 +103,38 @@ module.exports = React.createClass({ var attendeeList = this.getAttendeeList(); var buttonDisabled = this.state.attendee == null || !this.state.suggestion; return ( - - - - - - - - - - - - - - - + + + + Add Suggestion/Veto To: + + + + + + + Suggested By: + + + + + + +
+ +
+ +
); } }); diff --git a/js/components/RecordLunch.js b/js/components/RecordLunch.js index 8c9525a..05c1b77 100644 --- a/js/components/RecordLunch.js +++ b/js/components/RecordLunch.js @@ -6,6 +6,8 @@ var ControlLabel = ReactBootstrap.ControlLabel; var Grid = ReactBootstrap.Grid; var Row = ReactBootstrap.Row; var Col = ReactBootstrap.Col; +var Panel = ReactBootstrap.Panel; +var Button = ReactBootstrap.Button; var Multiselect = require('react-widgets').Multiselect; @@ -55,22 +57,47 @@ module.exports = React.createClass({ var attendeeList = this.getAttendeeList(); var suggestionIds = Object.keys(this.props.suggestions); - suggestionIds.sort(function(a, b){return parseInt(a, 10) - parseInt(b, 10);}); + suggestionIds.sort(function(a, b){return parseInt(b, 10) - parseInt(a, 10);}); var suggestions = []; for (var i in suggestionIds) { var suggestion = this.props.suggestions[suggestionIds[i]]; - suggestions.push(( - - {this.props.attendees[suggestion.AttendeeId].Name} - - {suggestion.RestaurantName} - - )); + var suggestorName = "Unknown"; + if (this.props.attendees.hasOwnProperty(suggestion.AttendeeId)) + suggestorName = this.props.attendees[suggestion.AttendeeId].Name; + + if (i == 0) { + var self = this; + var popFunction = function(){ + return function(){self.props.removeSuggestion(suggestion);}; + }(); + suggestions.push(( + + Current Suggestion: + + {suggestion.RestaurantName} (by {suggestorName}) + + + + + + )); + } else { + suggestions.push(( + + {(suggestionIds.length - i).toString() + "."} + + {suggestion.RestaurantName} (by {suggestorName}) + + + + + )); + } } return (
- + Attendees - - Suggested By: - - Restaurant Name: - - {suggestions} - - - + {suggestions} +
); } diff --git a/js/constants/SuggestionConstants.js b/js/constants/SuggestionConstants.js index fc96f40..002f84c 100644 --- a/js/constants/SuggestionConstants.js +++ b/js/constants/SuggestionConstants.js @@ -5,6 +5,8 @@ module.exports = keyMirror({ SUGGESTIONS_FETCHED: null, CREATE_SUGGESTION: null, SUGGESTION_CREATED: null, + REMOVE_SUGGESTION: null, + SUGGESTION_REMOVED: null, FETCH_POPULAR_SUGGESTIONS: null, POPULAR_SUGGESTIONS_FETCHED: null, }); diff --git a/js/containers/RecordLunchContainer.js b/js/containers/RecordLunchContainer.js index 755ff39..3914348 100644 --- a/js/containers/RecordLunchContainer.js +++ b/js/containers/RecordLunchContainer.js @@ -19,7 +19,8 @@ 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))} + createSuggestion: function(suggestion) {dispatch(SuggestionActions.create(suggestion))}, + removeSuggestion: function(suggestion) {dispatch(SuggestionActions.remove(suggestion))} } } diff --git a/js/reducers/SuggestionReducer.js b/js/reducers/SuggestionReducer.js index 38731ed..e6af852 100644 --- a/js/reducers/SuggestionReducer.js +++ b/js/reducers/SuggestionReducer.js @@ -18,6 +18,10 @@ module.exports = function(state = {}, action) { [suggestion.SuggestionId]: suggestion }); return suggestions; + case SuggestionConstants.SUGGESTION_REMOVED: + var suggestions = assign({}, state); + delete suggestions[action.suggestionId]; + return suggestions; case UserConstants.USER_LOGGEDOUT: return {}; default: diff --git a/static/index.html b/static/index.html index 078bb96..feb261d 100644 --- a/static/index.html +++ b/static/index.html @@ -1,6 +1,8 @@ - + + Lunch + diff --git a/static/stylesheet.css b/static/stylesheet.css index b9b9d40..780fbb3 100644 --- a/static/stylesheet.css +++ b/static/stylesheet.css @@ -6,7 +6,7 @@ div#content { width: 95%; height: 100%; min-width: 20em; - max-width: 100em; + max-width: 50em; margin: auto; } @@ -16,3 +16,8 @@ div.tab-content { border-left: 1px solid #ddd; border-right: 1px solid #ddd; } + +/* Make panels inside row line up with other rows by eliminating padding */ +div.row div.panel div.panel-body { + padding: 15px 0 15px 0 +} diff --git a/suggestions.go b/suggestions.go index 418880d..e4ca90b 100644 --- a/suggestions.go +++ b/suggestions.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "errors" "gopkg.in/gorp.v1" "log" "net/http" @@ -52,6 +53,11 @@ func (aeu SuggestionExistsError) Error() string { return "Suggestion exists" } +type SuggestionNotLastError struct{} + +func (snle SuggestionNotLastError) Error() string { + return "Suggestion not last on the stack" +} func (s *PopularSuggestion) Write(w http.ResponseWriter) error { enc := json.NewEncoder(w) return enc.Encode(s) @@ -82,6 +88,26 @@ func GetSuggestions(userid int64, date time.Time) (*[]*Suggestion, error) { return &suggestions, nil } +func GetSuggestion(suggestionid int64, userid int64, date time.Time) (*Suggestion, error) { + var s Suggestion + + err := DB.SelectOne(&s, "SELECT * from suggestions where UserId=? AND SuggestionId=? AND Date=?", userid, suggestionid, date) + if err != nil { + return nil, err + } + return &s, nil +} + +func VetoedBy(transaction *gorp.Transaction, suggestion *Suggestion) (*[]*Suggestion, error) { + var suggestions []*Suggestion + + _, err := transaction.Select(&suggestions, "SELECT * from suggestions WHERE UserId=? AND Date=? AND VetoingId=?", suggestion.UserId, suggestion.Date, suggestion.SuggestionId) + if err != nil { + return nil, err + } + return &suggestions, nil +} + func GetPopularSuggestions() (*[]*PopularSuggestion, error) { var suggestions []*Suggestion suggestionMap := make(map[string]int64) @@ -135,6 +161,42 @@ func InsertSuggestion(s *Suggestion) error { return nil } +func DeleteSuggestion(s *Suggestion) error { + transaction, err := DB.Begin() + if err != nil { + return err + } + + // Ensure suggestion hasn't been vetoed + suggestions, err := VetoedBy(transaction, s) + if err != nil { + transaction.Rollback() + return err + } + if len(*suggestions) > 0 { + transaction.Rollback() + return SuggestionNotLastError{} + } + + count, err := transaction.Delete(s) + if err != nil { + transaction.Rollback() + return err + } + if count != 1 { + transaction.Rollback() + return errors.New("Was going to delete more than one suggestion") + } + + err = transaction.Commit() + if err != nil { + transaction.Rollback() + return err + } + + return nil +} + func SuggestionHandler(w http.ResponseWriter, r *http.Request) { user, err := GetUserFromSession(r) if err != nil { @@ -196,8 +258,33 @@ func SuggestionHandler(w http.ResponseWriter, r *http.Request) { log.Print(err) return } + } else if r.Method == "DELETE" { + suggestionid, err := GetURLID(r.URL.Path) + if err != nil { + WriteError(w, 3 /* Invalid Request */) + return + } + + suggestion, err := GetSuggestion(suggestionid, user.UserId, today) + if err != nil { + WriteError(w, 3 /*Invalid Request*/) + return + } + + err = DeleteSuggestion(suggestion) + if err != nil { + if _, ok := err.(SuggestionNotLastError); ok { + WriteError(w, 8 /*Suggestion Not Last*/) + } else { + WriteError(w, 999 /*Internal Error*/) + log.Print(err) + } + return + } + + WriteSuccess(w) } else { - /* No PUT or DELETE */ + /* No PUT */ WriteError(w, 3 /*Invalid Request*/) return }