From 045e0530fb4182dbd76b99a8fd356b35901d34bc Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Sat, 14 Jan 2017 01:00:07 -0600 Subject: [PATCH 01/18] Added Attendees Response framework --- api/v1/models/Attendee.js | 12 ++++++---- api/v1/models/AttendeeRSVP.js | 22 +++++++++++++++++++ api/v1/requests/AttendeeRequest.js | 9 +++++++- api/v1/utils/rsvp.js | 21 ++++++++++++++++++ .../V20161226_0927__createAttendees.sql | 13 +++++++++++ ...V20161108_0927__createAttendees.revert.sql | 1 + 6 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 api/v1/models/AttendeeRSVP.js create mode 100644 api/v1/utils/rsvp.js diff --git a/api/v1/models/Attendee.js b/api/v1/models/Attendee.js index c4aee07..3676aef 100644 --- a/api/v1/models/Attendee.js +++ b/api/v1/models/Attendee.js @@ -10,6 +10,7 @@ var AttendeeProject = require('./AttendeeProject'); var AttendeeExtraInfo = require('./AttendeeExtraInfo'); var AttendeeEcosystemInterest = require('./AttendeeEcosystemInterest'); var AttendeeRequestedCollaborator = require('./AttendeeRequestedCollaborator'); +var AttendeeRSVP = require('./AttendeeRSVP'); var Attendee = Model.extend({ tableName: 'attendees', idAttribute: 'id', @@ -48,6 +49,9 @@ var Attendee = Model.extend({ }, collaborators: function () { return this.hasMany(AttendeeRequestedCollaborator); + }, + response : function () { + return this.hasOne(AttendeeRSVP); } }); @@ -58,7 +62,7 @@ var Attendee = Model.extend({ * @return {Promise} a Promise resolving to the resulting Attendee or null */ Attendee.findByUserId = function (userId) { - return Attendee.where({ user_id: userId }).fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators']}); + return Attendee.where({ user_id: userId }).fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'response']}); }; @@ -71,7 +75,7 @@ Attendee.fetchWithResumeByUserId = function (userId) { return Attendee.transaction(function (t){ var attendee; return Attendee.where({ user_id: userId }) - .fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators'], transacting: t}) + .fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'response'], transacting: t}) .then(function (a) { attendee = a; return Upload.where({ owner_id: userId, bucket: utils.storage.buckets.resumes }).fetch({transacting: t}); @@ -89,7 +93,7 @@ Attendee.fetchWithResumeByUserId = function (userId) { * @return {Promise} a Promise resolving to the resulting model or null */ Attendee.findById = function (id) { - return Attendee.where({ id: id }).fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators']}); + return Attendee.where({ id: id }).fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'response']}); }; /** @@ -101,7 +105,7 @@ Attendee.fetchWithResumeById = function (id) { return Attendee.transaction(function (t){ var attendee; return Attendee.where({ id: id }) - .fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators'], transacting: t}) + .fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'response'], transacting: t}) .then(function (a) { attendee = a; return Upload.where({ owner_id: a.get('userId'), bucket: utils.storage.buckets.resumes }).fetch({transacting: t}); diff --git a/api/v1/models/AttendeeRSVP.js b/api/v1/models/AttendeeRSVP.js new file mode 100644 index 0000000..3f2b30d --- /dev/null +++ b/api/v1/models/AttendeeRSVP.js @@ -0,0 +1,22 @@ +var _ = require('lodash'); + +var rsvp = require('../utils/rsvp') +var Model = require('./Model'); +var AttendeeRSVP = Model.extend({ + tableName: 'attendee_responses', + idAttribute: 'id', + validations: { + attendeeId: ['required', 'integer'], + attendeeResponse: ['required', 'string', rsvp.verifyResponse] + } +}); + +AttendeeRSVP.findByAttendeeId = function (attendeeId) { + return AttendeeRSVP.where({ attendee_id: attendeeId }).fetch(); +}; + +AttendeeRSVP.findById = function (id) { + return AttendeeRSVP.where({ id: id }).fetch(); +}; + +module.exports = AttendeeRSVP; diff --git a/api/v1/requests/AttendeeRequest.js b/api/v1/requests/AttendeeRequest.js index 660df4f..5a555e6 100644 --- a/api/v1/requests/AttendeeRequest.js +++ b/api/v1/requests/AttendeeRequest.js @@ -21,6 +21,11 @@ var requestedCollaboratorValidations = { collaborator: ['required', 'string', 'maxLength:255'] }; +var attendeeResponseValidatations = { + attendeeId: ['required', 'integer'], + attendeeResponse: ['required', 'string', rsvp.verifyResponse] +} + var bodyRequired = ['attendee', 'ecosystemInterests']; var bodyAllowed = ['projects', 'extras', 'collaborators']; var bodyValidations = { @@ -46,7 +51,8 @@ var bodyValidations = { 'ecosystemInterests': ['required', 'array', 'minLength:1', 'maxLength:2', validators.array(validators.nested(ecosystemInterestValidations, 'ecosystemInterests'), 'ecosystemInterests')], 'projects': ['array', 'maxLength:2', registration.verifyProjectArray, validators.array(validators.nested(projectValidations, 'projects'), 'projects')], 'extras': ['array', 'maxLength:3', validators.array(validators.nested(extraInfoValidations, 'extras'), 'extras')], - 'collaborators': ['array', 'maxLength:8', validators.array(validators.nested(requestedCollaboratorValidations, 'collaborators'), 'collaborators')] + 'collaborators': ['array', 'maxLength:8', validators.array(validators.nested(requestedCollaboratorValidations, 'collaborators'), 'collaborators')], + 'response': ['array', 'maxLength:1', validators.array(validators.nested(attendeeResponseValidatations, 'response'), 'response')] }; function AttendeeRequest(headers, body) { @@ -61,6 +67,7 @@ AttendeeRequest._extraInfoValidations = extraInfoValidations; AttendeeRequest._projectValidations = projectValidations; AttendeeRequest._ecosystemInterestValidations = ecosystemInterestValidations; AttendeeRequest._requestedCollaboratorValidations = requestedCollaboratorValidations; +AttendeeRequest._responseValidations = attendeeResponseValidatations; AttendeeRequest.prototype = Object.create(Request.prototype); AttendeeRequest.prototype.constructor = AttendeeRequest; diff --git a/api/v1/utils/rsvp.js b/api/v1/utils/rsvp.js new file mode 100644 index 0000000..efa4cce --- /dev/null +++ b/api/v1/utils/rsvp.js @@ -0,0 +1,21 @@ +var _ = require('lodash'); + +var ALL_RESPONSES = ['YES', 'NO', 'YESTOCREATE']; + +_.forEach(ALL_RESPONSES, function (response) { + module.exports[response] = response; +}); + +/** + * Ensures that the provided response is in ALL_RESPONSES + * @param {String} response the value to check + * @return {Boolean} true when the response is valid + * @throws TypeError when the response is invalid + */ +module.exports.verifyResponse = function (response) { + if (!module.exports.isIn(ALL_RESPONSES, response)) { + throw new TypeError(response + " is not a valid role"); + } + + return true; +}; diff --git a/database/migration/V20161226_0927__createAttendees.sql b/database/migration/V20161226_0927__createAttendees.sql index 8e0203e..9f17b1d 100644 --- a/database/migration/V20161226_0927__createAttendees.sql +++ b/database/migration/V20161226_0927__createAttendees.sql @@ -115,3 +115,16 @@ CREATE TABLE `attendee_project_interests` ( ON DELETE NO ACTION ON UPDATE NO ACTION ); + +CREATE TABLE `attendee_responses` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `attendee_id` INT UNSIGNED NOT NULL, + `response` ENUM('YES', 'NO', 'YESTOCREATE') NOT NULL, + PRIMARY KEY (`id`), + INDEX `fk_attendee_responses_attendee_id_idx` (`attendee_id` ASC), + CONSTRAINT `fk_attendee_responses_attendee_id` + FOREIGN KEY (`attendee_id`) + REFERENCES `attendees` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION +); \ No newline at end of file diff --git a/database/revert/V20161108_0927__createAttendees.revert.sql b/database/revert/V20161108_0927__createAttendees.revert.sql index 7e930e2..182e8ee 100644 --- a/database/revert/V20161108_0927__createAttendees.revert.sql +++ b/database/revert/V20161108_0927__createAttendees.revert.sql @@ -1,3 +1,4 @@ +DROP TABLE `attendee_responses`; DROP TABLE `attendee_project_interests`; DROP TABLE `attendee_projects`; DROP TABLE `attendee_extra_infos`; From 0a76c596033ed5cf2851e74db31ea6765ed64c77 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Sat, 14 Jan 2017 01:06:15 -0600 Subject: [PATCH 02/18] added missing snippet --- api/v1/requests/AttendeeRequest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1/requests/AttendeeRequest.js b/api/v1/requests/AttendeeRequest.js index 5a555e6..2f2a59b 100644 --- a/api/v1/requests/AttendeeRequest.js +++ b/api/v1/requests/AttendeeRequest.js @@ -27,7 +27,7 @@ var attendeeResponseValidatations = { } var bodyRequired = ['attendee', 'ecosystemInterests']; -var bodyAllowed = ['projects', 'extras', 'collaborators']; +var bodyAllowed = ['projects', 'extras', 'collaborators', 'response']; var bodyValidations = { 'attendee': ['required', 'plainObject'], 'attendee.firstName': ['required', 'string', 'maxLength:255'], From 35226e3fecc8e52a3c9460b236fb3b205fc3a923 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Sat, 14 Jan 2017 16:08:54 -0600 Subject: [PATCH 03/18] added missing import --- api/v1/requests/AttendeeRequest.js | 1 + 1 file changed, 1 insertion(+) diff --git a/api/v1/requests/AttendeeRequest.js b/api/v1/requests/AttendeeRequest.js index 2f2a59b..54e459a 100644 --- a/api/v1/requests/AttendeeRequest.js +++ b/api/v1/requests/AttendeeRequest.js @@ -1,6 +1,7 @@ var Request = require('./Request'); var validators = require('../utils/validators'); var registration = require('../utils/registration'); +var rsvp = require('../utils/rsvp'); var extraInfoValidations = { info: ['string', 'maxLength:255'] From fb05fe6f8c6ed0e8b842050a58980f15a7a6eccf Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Sat, 14 Jan 2017 17:09:52 -0600 Subject: [PATCH 04/18] Minor refactoring to response validations, response validator bugfix --- api/v1/models/Attendee.js | 4 +++- api/v1/requests/AttendeeRequest.js | 8 +------- api/v1/utils/rsvp.js | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/api/v1/models/Attendee.js b/api/v1/models/Attendee.js index 3676aef..dbc99e7 100644 --- a/api/v1/models/Attendee.js +++ b/api/v1/models/Attendee.js @@ -81,7 +81,9 @@ Attendee.fetchWithResumeByUserId = function (userId) { return Upload.where({ owner_id: userId, bucket: utils.storage.buckets.resumes }).fetch({transacting: t}); }) .then(function (u) { - attendee.set('resume', u.attributes); + if(u) + attendee.set('resume', u.attributes); + return attendee; }); }); diff --git a/api/v1/requests/AttendeeRequest.js b/api/v1/requests/AttendeeRequest.js index 54e459a..4172df2 100644 --- a/api/v1/requests/AttendeeRequest.js +++ b/api/v1/requests/AttendeeRequest.js @@ -22,11 +22,6 @@ var requestedCollaboratorValidations = { collaborator: ['required', 'string', 'maxLength:255'] }; -var attendeeResponseValidatations = { - attendeeId: ['required', 'integer'], - attendeeResponse: ['required', 'string', rsvp.verifyResponse] -} - var bodyRequired = ['attendee', 'ecosystemInterests']; var bodyAllowed = ['projects', 'extras', 'collaborators', 'response']; var bodyValidations = { @@ -53,7 +48,7 @@ var bodyValidations = { 'projects': ['array', 'maxLength:2', registration.verifyProjectArray, validators.array(validators.nested(projectValidations, 'projects'), 'projects')], 'extras': ['array', 'maxLength:3', validators.array(validators.nested(extraInfoValidations, 'extras'), 'extras')], 'collaborators': ['array', 'maxLength:8', validators.array(validators.nested(requestedCollaboratorValidations, 'collaborators'), 'collaborators')], - 'response': ['array', 'maxLength:1', validators.array(validators.nested(attendeeResponseValidatations, 'response'), 'response')] + 'response': ['string', rsvp.verifyResponse] }; function AttendeeRequest(headers, body) { @@ -68,7 +63,6 @@ AttendeeRequest._extraInfoValidations = extraInfoValidations; AttendeeRequest._projectValidations = projectValidations; AttendeeRequest._ecosystemInterestValidations = ecosystemInterestValidations; AttendeeRequest._requestedCollaboratorValidations = requestedCollaboratorValidations; -AttendeeRequest._responseValidations = attendeeResponseValidatations; AttendeeRequest.prototype = Object.create(Request.prototype); AttendeeRequest.prototype.constructor = AttendeeRequest; diff --git a/api/v1/utils/rsvp.js b/api/v1/utils/rsvp.js index efa4cce..f2e1433 100644 --- a/api/v1/utils/rsvp.js +++ b/api/v1/utils/rsvp.js @@ -13,8 +13,8 @@ _.forEach(ALL_RESPONSES, function (response) { * @throws TypeError when the response is invalid */ module.exports.verifyResponse = function (response) { - if (!module.exports.isIn(ALL_RESPONSES, response)) { - throw new TypeError(response + " is not a valid role"); + if (!_.includes(ALL_RESPONSES, response.attendeeResponse)) { + throw new TypeError(response + " is not a valid response"); } return true; From 9ea657ea8adfcf0a8b1e1e1b57fd18e5acaa4969 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Sat, 14 Jan 2017 20:34:04 -0600 Subject: [PATCH 05/18] refactoring, validator modification, added missing index declarations --- api/v1/models/Attendee.js | 10 +++++----- api/v1/models/index.js | 1 + api/v1/requests/AttendeeRequest.js | 5 +++-- api/v1/utils/index.js | 1 + api/v1/utils/rsvp.js | 4 ++-- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/api/v1/models/Attendee.js b/api/v1/models/Attendee.js index dbc99e7..0025717 100644 --- a/api/v1/models/Attendee.js +++ b/api/v1/models/Attendee.js @@ -50,7 +50,7 @@ var Attendee = Model.extend({ collaborators: function () { return this.hasMany(AttendeeRequestedCollaborator); }, - response : function () { + reply : function () { return this.hasOne(AttendeeRSVP); } }); @@ -62,7 +62,7 @@ var Attendee = Model.extend({ * @return {Promise} a Promise resolving to the resulting Attendee or null */ Attendee.findByUserId = function (userId) { - return Attendee.where({ user_id: userId }).fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'response']}); + return Attendee.where({ user_id: userId }).fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'reply']}); }; @@ -75,7 +75,7 @@ Attendee.fetchWithResumeByUserId = function (userId) { return Attendee.transaction(function (t){ var attendee; return Attendee.where({ user_id: userId }) - .fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'response'], transacting: t}) + .fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'reply'], transacting: t}) .then(function (a) { attendee = a; return Upload.where({ owner_id: userId, bucket: utils.storage.buckets.resumes }).fetch({transacting: t}); @@ -95,7 +95,7 @@ Attendee.fetchWithResumeByUserId = function (userId) { * @return {Promise} a Promise resolving to the resulting model or null */ Attendee.findById = function (id) { - return Attendee.where({ id: id }).fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'response']}); + return Attendee.where({ id: id }).fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'reply']}); }; /** @@ -107,7 +107,7 @@ Attendee.fetchWithResumeById = function (id) { return Attendee.transaction(function (t){ var attendee; return Attendee.where({ id: id }) - .fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'response'], transacting: t}) + .fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'reply'], transacting: t}) .then(function (a) { attendee = a; return Upload.where({ owner_id: a.get('userId'), bucket: utils.storage.buckets.resumes }).fetch({transacting: t}); diff --git a/api/v1/models/index.js b/api/v1/models/index.js index ace45dd..d08a8d7 100644 --- a/api/v1/models/index.js +++ b/api/v1/models/index.js @@ -4,6 +4,7 @@ module.exports = { AttendeeProject: require('./AttendeeProject'), AttendeeProjectInterest: require('./AttendeeProjectInterest'), AttendeeRequestedCollaborator: require('./AttendeeRequestedCollaborator'), + AttendeeRSVP: require('./AttendeeRSVP'), Project: require('./Project'), Ecosystem: require('./Ecosystem'), MailingList: require('./MailingList'), diff --git a/api/v1/requests/AttendeeRequest.js b/api/v1/requests/AttendeeRequest.js index 4172df2..be83db7 100644 --- a/api/v1/requests/AttendeeRequest.js +++ b/api/v1/requests/AttendeeRequest.js @@ -23,7 +23,7 @@ var requestedCollaboratorValidations = { }; var bodyRequired = ['attendee', 'ecosystemInterests']; -var bodyAllowed = ['projects', 'extras', 'collaborators', 'response']; +var bodyAllowed = ['projects', 'extras', 'collaborators', 'reply']; var bodyValidations = { 'attendee': ['required', 'plainObject'], 'attendee.firstName': ['required', 'string', 'maxLength:255'], @@ -48,7 +48,8 @@ var bodyValidations = { 'projects': ['array', 'maxLength:2', registration.verifyProjectArray, validators.array(validators.nested(projectValidations, 'projects'), 'projects')], 'extras': ['array', 'maxLength:3', validators.array(validators.nested(extraInfoValidations, 'extras'), 'extras')], 'collaborators': ['array', 'maxLength:8', validators.array(validators.nested(requestedCollaboratorValidations, 'collaborators'), 'collaborators')], - 'response': ['string', rsvp.verifyResponse] + 'reply': ['plainObject'], + 'reply.attendeeResponse': ['required', 'string', rsvp.verifyReply] }; function AttendeeRequest(headers, body) { diff --git a/api/v1/utils/index.js b/api/v1/utils/index.js index a2b4601..79b8129 100644 --- a/api/v1/utils/index.js +++ b/api/v1/utils/index.js @@ -8,6 +8,7 @@ module.exports = { storage: require('./storage.js'), time: require('./time.js'), registration: require('./registration.js'), + rsvp: require('./rsvp.js'), validators: require('./validators.js'), cache: require("./cache.js") }; diff --git a/api/v1/utils/rsvp.js b/api/v1/utils/rsvp.js index f2e1433..c969048 100644 --- a/api/v1/utils/rsvp.js +++ b/api/v1/utils/rsvp.js @@ -12,8 +12,8 @@ _.forEach(ALL_RESPONSES, function (response) { * @return {Boolean} true when the response is valid * @throws TypeError when the response is invalid */ -module.exports.verifyResponse = function (response) { - if (!_.includes(ALL_RESPONSES, response.attendeeResponse)) { +module.exports.verifyReply = function (response) { + if (!_.includes(ALL_RESPONSES, response)) { throw new TypeError(response + " is not a valid response"); } From 9c02fdfa63d0887b6dc690a87f27fc53dbaf9027 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Sat, 14 Jan 2017 21:24:52 -0600 Subject: [PATCH 06/18] Overhaul --- api/v1/models/Attendee.js | 15 ++++++++----- api/v1/models/AttendeeRSVP.js | 22 +++++++++++++++++++ api/v1/models/index.js | 1 + api/v1/requests/AttendeeRequest.js | 11 ++++++++-- api/v1/utils/index.js | 1 + api/v1/utils/rsvp.js | 11 ++++++++++ .../V20161226_0927__createAttendees.sql | 13 +++++++++++ ...170114_2116__createAttendeeRSVP.revert.sql | 1 + 8 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 api/v1/models/AttendeeRSVP.js create mode 100644 api/v1/utils/rsvp.js create mode 100644 database/revert/V20170114_2116__createAttendeeRSVP.revert.sql diff --git a/api/v1/models/Attendee.js b/api/v1/models/Attendee.js index c4aee07..881fd73 100644 --- a/api/v1/models/Attendee.js +++ b/api/v1/models/Attendee.js @@ -10,6 +10,7 @@ var AttendeeProject = require('./AttendeeProject'); var AttendeeExtraInfo = require('./AttendeeExtraInfo'); var AttendeeEcosystemInterest = require('./AttendeeEcosystemInterest'); var AttendeeRequestedCollaborator = require('./AttendeeRequestedCollaborator'); +var AttendeeRSVP = require('./AttendeeRSVP'); var Attendee = Model.extend({ tableName: 'attendees', idAttribute: 'id', @@ -48,6 +49,9 @@ var Attendee = Model.extend({ }, collaborators: function () { return this.hasMany(AttendeeRequestedCollaborator); + }, + rsvp: function () { + return this.hasOne(AttendeeRSVP); } }); @@ -58,7 +62,7 @@ var Attendee = Model.extend({ * @return {Promise} a Promise resolving to the resulting Attendee or null */ Attendee.findByUserId = function (userId) { - return Attendee.where({ user_id: userId }).fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators']}); + return Attendee.where({ user_id: userId }).fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'rsvp']}); }; @@ -71,13 +75,14 @@ Attendee.fetchWithResumeByUserId = function (userId) { return Attendee.transaction(function (t){ var attendee; return Attendee.where({ user_id: userId }) - .fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators'], transacting: t}) + .fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'rsvp'], transacting: t}) .then(function (a) { attendee = a; return Upload.where({ owner_id: userId, bucket: utils.storage.buckets.resumes }).fetch({transacting: t}); }) .then(function (u) { - attendee.set('resume', u.attributes); + if(u) + attendee.set('resume', u.attributes); return attendee; }); }); @@ -89,7 +94,7 @@ Attendee.fetchWithResumeByUserId = function (userId) { * @return {Promise} a Promise resolving to the resulting model or null */ Attendee.findById = function (id) { - return Attendee.where({ id: id }).fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators']}); + return Attendee.where({ id: id }).fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'rsvp']}); }; /** @@ -101,7 +106,7 @@ Attendee.fetchWithResumeById = function (id) { return Attendee.transaction(function (t){ var attendee; return Attendee.where({ id: id }) - .fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators'], transacting: t}) + .fetch({withRelated: ['projects', 'ecosystemInterests', 'extras', 'collaborators', 'rsvp'], transacting: t}) .then(function (a) { attendee = a; return Upload.where({ owner_id: a.get('userId'), bucket: utils.storage.buckets.resumes }).fetch({transacting: t}); diff --git a/api/v1/models/AttendeeRSVP.js b/api/v1/models/AttendeeRSVP.js new file mode 100644 index 0000000..f0e2cb9 --- /dev/null +++ b/api/v1/models/AttendeeRSVP.js @@ -0,0 +1,22 @@ +var _ = require('lodash'); + +var rsvp = require('../utils/rsvp'); +var Model = require('./Model'); +var AttendeeRSVP = Model.extend({ + tableName: 'attendee_rsvps', + idAttribute: 'id', + validations: { + attendeeId: ['required', 'integer'], + attendeeAttendance: ['required', 'string', rsvp.verifyAttendanceReply] + } +}); + +AttendeeRSVP.findByAttendeeId = function (attendeeId) { + return AttendeeRSVP.where({ attendee_id: attendeeId }).fetch(); +}; + +AttendeeRSVP.findById = function (id) { + return AttendeeRSVP.where({ id: id }).fetch(); +}; + +module.exports = AttendeeRSVP; \ No newline at end of file diff --git a/api/v1/models/index.js b/api/v1/models/index.js index ace45dd..d08a8d7 100644 --- a/api/v1/models/index.js +++ b/api/v1/models/index.js @@ -4,6 +4,7 @@ module.exports = { AttendeeProject: require('./AttendeeProject'), AttendeeProjectInterest: require('./AttendeeProjectInterest'), AttendeeRequestedCollaborator: require('./AttendeeRequestedCollaborator'), + AttendeeRSVP: require('./AttendeeRSVP'), Project: require('./Project'), Ecosystem: require('./Ecosystem'), MailingList: require('./MailingList'), diff --git a/api/v1/requests/AttendeeRequest.js b/api/v1/requests/AttendeeRequest.js index 660df4f..d77f650 100644 --- a/api/v1/requests/AttendeeRequest.js +++ b/api/v1/requests/AttendeeRequest.js @@ -1,6 +1,7 @@ var Request = require('./Request'); var validators = require('../utils/validators'); var registration = require('../utils/registration'); +var rsvp = require('../utils/rsvp'); var extraInfoValidations = { info: ['string', 'maxLength:255'] @@ -21,8 +22,12 @@ var requestedCollaboratorValidations = { collaborator: ['required', 'string', 'maxLength:255'] }; +var attendeeRSVPValidations = { + attendeeAttendance: ['required', 'string', rsvp.verifyAttendanceReply] +}; + var bodyRequired = ['attendee', 'ecosystemInterests']; -var bodyAllowed = ['projects', 'extras', 'collaborators']; +var bodyAllowed = ['projects', 'extras', 'collaborators', 'rsvp']; var bodyValidations = { 'attendee': ['required', 'plainObject'], 'attendee.firstName': ['required', 'string', 'maxLength:255'], @@ -46,7 +51,8 @@ var bodyValidations = { 'ecosystemInterests': ['required', 'array', 'minLength:1', 'maxLength:2', validators.array(validators.nested(ecosystemInterestValidations, 'ecosystemInterests'), 'ecosystemInterests')], 'projects': ['array', 'maxLength:2', registration.verifyProjectArray, validators.array(validators.nested(projectValidations, 'projects'), 'projects')], 'extras': ['array', 'maxLength:3', validators.array(validators.nested(extraInfoValidations, 'extras'), 'extras')], - 'collaborators': ['array', 'maxLength:8', validators.array(validators.nested(requestedCollaboratorValidations, 'collaborators'), 'collaborators')] + 'collaborators': ['array', 'maxLength:8', validators.array(validators.nested(requestedCollaboratorValidations, 'collaborators'), 'collaborators')], + 'rsvp': ['array', 'maxLength:1', validators.array(validators.nested(attendeeRSVPValidations, 'rsvp'), 'rsvp')] }; function AttendeeRequest(headers, body) { @@ -61,6 +67,7 @@ AttendeeRequest._extraInfoValidations = extraInfoValidations; AttendeeRequest._projectValidations = projectValidations; AttendeeRequest._ecosystemInterestValidations = ecosystemInterestValidations; AttendeeRequest._requestedCollaboratorValidations = requestedCollaboratorValidations; +AttendeeRequest._attendeeRSVPValidations = attendeeRSVPValidations; AttendeeRequest.prototype = Object.create(Request.prototype); AttendeeRequest.prototype.constructor = AttendeeRequest; diff --git a/api/v1/utils/index.js b/api/v1/utils/index.js index a2b4601..2cf93f2 100644 --- a/api/v1/utils/index.js +++ b/api/v1/utils/index.js @@ -4,6 +4,7 @@ module.exports = { mail: require('./mail.js'), logs: require('./logs.js'), roles: require('./roles.js'), + rsvp: require('./rsvp.js'), scopes: require('./scopes.js'), storage: require('./storage.js'), time: require('./time.js'), diff --git a/api/v1/utils/rsvp.js b/api/v1/utils/rsvp.js new file mode 100644 index 0000000..08043b8 --- /dev/null +++ b/api/v1/utils/rsvp.js @@ -0,0 +1,11 @@ +var _ = require('lodash'); + +var ATTENDANCE_REPLY = ['YES', 'NO', 'YES_TO_CREATE']; + +module.exports.verifyAttendanceReply = function(reply){ + if (!_.includes(ATTENDANCE_REPLY, reply)) { + throw new TypeError(reply + " is not a valid attendance reply option"); + } + + return true; +} \ No newline at end of file diff --git a/database/migration/V20161226_0927__createAttendees.sql b/database/migration/V20161226_0927__createAttendees.sql index 8e0203e..cdcfb15 100644 --- a/database/migration/V20161226_0927__createAttendees.sql +++ b/database/migration/V20161226_0927__createAttendees.sql @@ -115,3 +115,16 @@ CREATE TABLE `attendee_project_interests` ( ON DELETE NO ACTION ON UPDATE NO ACTION ); + +CREATE TABLE `attendee_rsvps` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `attendee_id` INT UNSIGNED NOT NULL, + `attendee_attendance` ENUM('YES', 'NO', 'YES_TO_CREATE') NOT NULL, + PRIMARY KEY (`id`), + INDEX `fk_attendee_rsvps_attendee_id_idx` (`attendee_id` ASC), + CONSTRAINT `fk_attendee_rsvps_attendee_id` + FOREIGN KEY (`attendee_id`) + REFERENCES `attendees` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION +); \ No newline at end of file diff --git a/database/revert/V20170114_2116__createAttendeeRSVP.revert.sql b/database/revert/V20170114_2116__createAttendeeRSVP.revert.sql new file mode 100644 index 0000000..0b00280 --- /dev/null +++ b/database/revert/V20170114_2116__createAttendeeRSVP.revert.sql @@ -0,0 +1 @@ +DROP TABLE `attendee_rsvps`; \ No newline at end of file From d914e559e977e6abd15b479ce48435fe94f264f9 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Sat, 14 Jan 2017 22:54:11 -0600 Subject: [PATCH 07/18] Finalized attendance status --- api/v1/models/Attendee.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1/models/Attendee.js b/api/v1/models/Attendee.js index 881fd73..9e95ae8 100644 --- a/api/v1/models/Attendee.js +++ b/api/v1/models/Attendee.js @@ -51,7 +51,7 @@ var Attendee = Model.extend({ return this.hasMany(AttendeeRequestedCollaborator); }, rsvp: function () { - return this.hasOne(AttendeeRSVP); + return this.hasMany(AttendeeRSVP); } }); From eb6194397409795ed2594d451f8dc66bdd956d21 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Mon, 16 Jan 2017 04:24:22 -0600 Subject: [PATCH 08/18] Moved RSVP functionality into its own endpoints --- api/v1/controllers/RSVPController.js | 146 ++++++++++++++++++ api/v1/controllers/index.js | 5 +- api/v1/index.js | 4 +- api/v1/models/Attendee.js | 2 +- api/v1/models/User.js | 4 +- api/v1/requests/AttendeeRequest.js | 11 +- api/v1/requests/RSVPRequest.js | 19 +++ api/v1/requests/index.js | 3 +- api/v1/services/RSVPService | 57 +++++++ api/v1/services/index.js | 3 +- api/v1/utils/mail.js | 6 +- ...20160712_1603__createMailingListTables.sql | 4 +- 12 files changed, 239 insertions(+), 25 deletions(-) create mode 100644 api/v1/controllers/RSVPController.js create mode 100644 api/v1/requests/RSVPRequest.js create mode 100644 api/v1/services/RSVPService diff --git a/api/v1/controllers/RSVPController.js b/api/v1/controllers/RSVPController.js new file mode 100644 index 0000000..a2012d8 --- /dev/null +++ b/api/v1/controllers/RSVPController.js @@ -0,0 +1,146 @@ +var bodyParser = require('body-parser'); + +var services = require('../services'); +var middleware = require('../middleware'); +var requests = require('../requests'); +var roles = require('../utils/roles'); +var mail = require('../utils/mail'); +var User = require('../models/User'); + +var router = require('express').Router(); + +function _isAuthenticated (req) { + return req.auth && (req.user !== undefined); +} + +function _removeFromList(rsvpCurrent, rsvpNew) { + if(rsvpCurrent.get('attendeeAttendance') !== 'NO' && rsvpNew.attendeeAttendance === 'NO') + return true; + return false; +} + +function _addToList(rsvpCurrent, rsvpNew) { + if(rsvpCurrent.get('attendeeAttendance') === 'NO' && rsvpNew.attendeeAttendance !== 'NO') + return true; + return false; +} + +function createRSVP(req, res, next) { + services.RegistrationService + .findAttendeeByUser(req.user) + .then(function(attendee) { + services.RSVPService + .createRSVP(attendee, req.user, req.body) + .then(function (rsvp) { + if(rsvp.get('attendeeAttendance') !== 'NO') + services.MailService.addToList(req.user, mail.lists.attendees); + res.body = rsvp.toJSON(); + + return next(); + }) + }) + .catch(function(error) { + return next(error); + }); +} + +function fetchRSVPByUser(req, res, next) { + services.RegistrationService + .findAttendeeByUser(req.user) + .then(function(attendee) { + services.RSVPService + .findRSVPByAttendee(attendee) + .then(function(rsvp){ + res.body = rsvp.toJSON(); + + return next(); + }) + }) + .catch(function(error) { + return next(error); + }) +} + +function fetchRSVPByAttendeeId(req, res, next) { + services.RegistrationService + .findAttendeeById(req.params.id) + .then(function(attendee) { + services.RSVPService + .findRSVPByAttendee(attendee) + .then(function(rsvp){ + res.body = rsvp.toJSON(); + + return next(); + }) + }) + .catch(function(error) { + return next(error); + }) +} + +function updateRSVPByUser(req, res, next) { + services.RegistrationService + .findAttendeeByUser(req.user) + .then(function(attendee) { + services.RSVPService + .findRSVPByAttendee(attendee) + .then(function (rsvp) { + if(_addToList(rsvp, req.body)) + services.MailService.addToList(req.user, mail.lists.attendees); + if(_removeFromList(rsvp, req.body)) + services.MailService.removeFromList(req.user, mail.lists.attendees); + + return services.RSVPService.updateRSVP(rsvp, req.body); + }) + .then(function(rsvp){ + res.body = rsvp.toJSON(); + + return next(); + }) + }) + .catch(function (error) { + return next(error); + }); +} + +function updateRSVPById(req, res, next) { + services.RegistrationService + .findAttendeeById(req.params.id) + .then(function(attendee) { + services.RSVPService + .findRSVPByAttendee(attendee) + .then(function (rsvp) { + if(_addToList(rsvp, req.body)) + services.MailService.addToList(req.user, mail.lists.attendees); + if(_removeFromList(rsvp, req.body)) + services.MailService.removeFromList(req.user, mail.lists.attendees); + + return services.RSVPService.updateRSVP(rsvp, req.body); + }) + .then(function(rsvp){ + res.body = rsvp.toJSON(); + + return next(); + }) + }) + .catch(function (error) { + return next(error); + }); +} + +router.use(bodyParser.json()); +router.use(middleware.auth); + +router.post('/', middleware.request(requests.RSVPRequest), + middleware.permission(roles.NONE, _isAuthenticated), createRSVP); +router.get('/', middleware.permission(roles.ATTENDEE), fetchRSVPByUser); +router.get('/:id', middleware.permission(roles.ORGANIZERS), fetchRSVPByAttendeeId); +router.put('/', middleware.request(requests.RSVPRequest), + middleware.permission(roles.ATTENDEE), updateRSVPByUser); +router.put('/:id', middleware.request(requests.RSVPRequest), + middleware.permission(roles.ORGANIZERS), updateRSVPById); + +router.use(middleware.response); +router.use(middleware.errors); + +module.exports.router = router; \ No newline at end of file diff --git a/api/v1/controllers/index.js b/api/v1/controllers/index.js index 317b8dd..55930db 100644 --- a/api/v1/controllers/index.js +++ b/api/v1/controllers/index.js @@ -5,5 +5,6 @@ module.exports = { UserController: require('./UserController.js'), RegistrationController: require('./RegistrationController.js'), ProjectController: require('./ProjectController.js'), - HealthController: require('./HealthController.js') -}; + HealthController: require('./HealthController.js'), + RSVPController: require('./RSVPController.js') +}; \ No newline at end of file diff --git a/api/v1/index.js b/api/v1/index.js index 9168b77..b11ab14 100644 --- a/api/v1/index.js +++ b/api/v1/index.js @@ -19,6 +19,6 @@ v1.use('/registration', controllers.RegistrationController.router); v1.use('/project', controllers.ProjectController.router); v1.use('/ecosystem', controllers.EcosystemController.router); v1.use('/health', controllers.HealthController.router); +v1.use('/rsvp', controllers.RSVPController.router); - -module.exports = v1; +module.exports = v1; \ No newline at end of file diff --git a/api/v1/models/Attendee.js b/api/v1/models/Attendee.js index 9e95ae8..881fd73 100644 --- a/api/v1/models/Attendee.js +++ b/api/v1/models/Attendee.js @@ -51,7 +51,7 @@ var Attendee = Model.extend({ return this.hasMany(AttendeeRequestedCollaborator); }, rsvp: function () { - return this.hasMany(AttendeeRSVP); + return this.hasOne(AttendeeRSVP); } }); diff --git a/api/v1/models/User.js b/api/v1/models/User.js index 21bb95b..666a764 100644 --- a/api/v1/models/User.js +++ b/api/v1/models/User.js @@ -99,8 +99,8 @@ User.prototype.setPassword = function (password) { * @return {UserRole} the desired role, or undefined */ User.prototype.getRole = function (role) { - return _.find(this.related('roles').models, function (role) { - return role.role === role; + return _.find(this.related('roles').models, function (roleInUser) { + return roleInUser.get('role') === role; }); }; diff --git a/api/v1/requests/AttendeeRequest.js b/api/v1/requests/AttendeeRequest.js index d77f650..660df4f 100644 --- a/api/v1/requests/AttendeeRequest.js +++ b/api/v1/requests/AttendeeRequest.js @@ -1,7 +1,6 @@ var Request = require('./Request'); var validators = require('../utils/validators'); var registration = require('../utils/registration'); -var rsvp = require('../utils/rsvp'); var extraInfoValidations = { info: ['string', 'maxLength:255'] @@ -22,12 +21,8 @@ var requestedCollaboratorValidations = { collaborator: ['required', 'string', 'maxLength:255'] }; -var attendeeRSVPValidations = { - attendeeAttendance: ['required', 'string', rsvp.verifyAttendanceReply] -}; - var bodyRequired = ['attendee', 'ecosystemInterests']; -var bodyAllowed = ['projects', 'extras', 'collaborators', 'rsvp']; +var bodyAllowed = ['projects', 'extras', 'collaborators']; var bodyValidations = { 'attendee': ['required', 'plainObject'], 'attendee.firstName': ['required', 'string', 'maxLength:255'], @@ -51,8 +46,7 @@ var bodyValidations = { 'ecosystemInterests': ['required', 'array', 'minLength:1', 'maxLength:2', validators.array(validators.nested(ecosystemInterestValidations, 'ecosystemInterests'), 'ecosystemInterests')], 'projects': ['array', 'maxLength:2', registration.verifyProjectArray, validators.array(validators.nested(projectValidations, 'projects'), 'projects')], 'extras': ['array', 'maxLength:3', validators.array(validators.nested(extraInfoValidations, 'extras'), 'extras')], - 'collaborators': ['array', 'maxLength:8', validators.array(validators.nested(requestedCollaboratorValidations, 'collaborators'), 'collaborators')], - 'rsvp': ['array', 'maxLength:1', validators.array(validators.nested(attendeeRSVPValidations, 'rsvp'), 'rsvp')] + 'collaborators': ['array', 'maxLength:8', validators.array(validators.nested(requestedCollaboratorValidations, 'collaborators'), 'collaborators')] }; function AttendeeRequest(headers, body) { @@ -67,7 +61,6 @@ AttendeeRequest._extraInfoValidations = extraInfoValidations; AttendeeRequest._projectValidations = projectValidations; AttendeeRequest._ecosystemInterestValidations = ecosystemInterestValidations; AttendeeRequest._requestedCollaboratorValidations = requestedCollaboratorValidations; -AttendeeRequest._attendeeRSVPValidations = attendeeRSVPValidations; AttendeeRequest.prototype = Object.create(Request.prototype); AttendeeRequest.prototype.constructor = AttendeeRequest; diff --git a/api/v1/requests/RSVPRequest.js b/api/v1/requests/RSVPRequest.js new file mode 100644 index 0000000..b3f0fae --- /dev/null +++ b/api/v1/requests/RSVPRequest.js @@ -0,0 +1,19 @@ +var Request = require('./Request'); +var rsvp = require('../utils/rsvp'); + +var bodyRequired = ['attendeeAttendance']; +var bodyValidations = { + 'attendeeAttendance': ['required', 'string', rsvp.verifyAttendanceReply] +}; + +function RSVPRequest(headers, body) { + Request.call(this, headers, body); + + this.bodyRequired = bodyRequired; + this.bodyValidations = bodyValidations; +} + +RSVPRequest.prototype = Object.create(Request.prototype); +RSVPRequest.prototype.constructor = RSVPRequest; + +module.exports = RSVPRequest; diff --git a/api/v1/requests/index.js b/api/v1/requests/index.js index 0ad05d1..a0f7585 100644 --- a/api/v1/requests/index.js +++ b/api/v1/requests/index.js @@ -8,5 +8,6 @@ module.exports = { ProjectMentorRequest: require('./ProjectMentorRequest'), ResetTokenRequest: require('./ResetTokenRequest'), ResetPasswordRequest: require('./ResetPasswordRequest'), - UploadRequest: require('./UploadRequest') + UploadRequest: require('./UploadRequest'), + RSVPRequest: require('./RSVPRequest') }; diff --git a/api/v1/services/RSVPService b/api/v1/services/RSVPService new file mode 100644 index 0000000..2ada1c6 --- /dev/null +++ b/api/v1/services/RSVPService @@ -0,0 +1,57 @@ +var CheckitError = require('checkit').Error; +var _Promise = require('bluebird'); +var _ = require('lodash'); + +var RSVP = require('../models/AttendeeRSVP'); +var UserRole = require('../models/UserRole'); +var errors = require('../errors'); +var utils = require('../utils'); + +module.exports.createRSVP = function (attendee, user, attributes) { + attributes.attendeeId = attendee.get('id'); + var rsvp = RSVP.forge(attributes); + + return rsvp + .validate() + .catch(CheckitError, utils.errors.handleValidationError) + .then(function (validated) { + return RSVP.findByAttendeeId(attributes.attendeeId); + }) + .then(function (result) { + if (!_.isNull(result)) { + var message = "An RSVP already exists for the given attendee"; + var source = "attendeeId"; + throw new errors.InvalidParameterError(message, source); + } + + var userRole = user.getRole(utils.roles.ATTENDEE) + UserRole.setActive(userRole, true); + + return rsvp.save(); + }) +}; + +module.exports.findRSVPByAttendee = function (attendee) { + return RSVP + .findByAttendeeId(attendee.get('id')) + .then(function (result) { + if (_.isNull(result)) { + var message = "An RSVP cannot be found for the given attendee"; + var source = "attendeeId"; + throw new errors.NotFoundError(message, source); + } + + return _Promise.resolve(result); + }); +}; + +module.exports.updateRSVP = function (rsvp, attributes) { + rsvp.set(attributes); + + return rsvp + .validate() + .catch(CheckitError, utils.errors.handleValidationError) + .then(function (validated) { + return rsvp.save(); + }); +}; \ No newline at end of file diff --git a/api/v1/services/index.js b/api/v1/services/index.js index 1fa29c6..2d1a806 100644 --- a/api/v1/services/index.js +++ b/api/v1/services/index.js @@ -7,5 +7,6 @@ module.exports = { RegistrationService: require('./RegistrationService'), StorageService: require('./StorageService'), UserService: require('./UserService'), - TokenService: require('./TokenService') + TokenService: require('./TokenService'), + RSVPService: require('./RSVPService') }; diff --git a/api/v1/utils/mail.js b/api/v1/utils/mail.js index 6093bdc..7a0c3df 100644 --- a/api/v1/utils/mail.js +++ b/api/v1/utils/mail.js @@ -11,11 +11,9 @@ module.exports.lists.idlers = { name: 'idlers', id: 'idlers-2017' }; module.exports.lists.applicants = { name: 'applicants', id: 'applicants-2017' }; module.exports.lists.accepted = { name: 'accepted', id: 'accepted-2017' }; module.exports.lists.waitlisted = { name: 'waitlisted', id: 'waitlisted-2017' }; -module.exports.lists.software = { name: 'software', id: 'software-2017' }; -module.exports.lists.hardware = { name: 'hardware', id: 'hardware-2017' }; -module.exports.lists.open_source = { name: 'open_source', id: 'open-source-2017' }; +module.exports.lists.attendees = {name: 'attendees', id: 'attendees-2017'}; module.exports.lists.admins = { name: 'admins', id: 'admins-2017' }; module.exports.lists.staff = { name: 'staff', id: 'staff-2017' }; module.exports.lists.sponsors = { name: 'sponsors', id: 'sponsors-2017' }; module.exports.lists.mentors = { name: 'mentors', id: 'mentors-2017' }; -module.exports.lists.volunteers = { name: 'volunteers', id: 'volunteers-2017' }; +module.exports.lists.volunteers = { name: 'volunteers', id: 'volunteers-2017' }; \ No newline at end of file diff --git a/database/migration/V20160712_1603__createMailingListTables.sql b/database/migration/V20160712_1603__createMailingListTables.sql index 0e837aa..a3b7497 100644 --- a/database/migration/V20160712_1603__createMailingListTables.sql +++ b/database/migration/V20160712_1603__createMailingListTables.sql @@ -26,9 +26,7 @@ INSERT INTO `mailing_lists` (`name`) VALUES ('idlers'); INSERT INTO `mailing_lists` (`name`) VALUES ('applicants'); INSERT INTO `mailing_lists` (`name`) VALUES ('accepted'); INSERT INTO `mailing_lists` (`name`) VALUES ('waitlisted'); -INSERT INTO `mailing_lists` (`name`) VALUES ('software'); -INSERT INTO `mailing_lists` (`name`) VALUES ('hardware'); -INSERT INTO `mailing_lists` (`name`) VALUES ('open_source'); +INSERT INTO `mailing_lists` (`name`) VALUES ('attendees'); INSERT INTO `mailing_lists` (`name`) VALUES ('admins'); INSERT INTO `mailing_lists` (`name`) VALUES ('staff'); INSERT INTO `mailing_lists` (`name`) VALUES ('sponsors'); From df372317003286f61ef712e6c380056a63d8d45a Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Mon, 16 Jan 2017 04:30:57 -0600 Subject: [PATCH 09/18] Added docs to RSVPService --- api/v1/services/RSVPService | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/api/v1/services/RSVPService b/api/v1/services/RSVPService index 2ada1c6..77253a4 100644 --- a/api/v1/services/RSVPService +++ b/api/v1/services/RSVPService @@ -7,6 +7,14 @@ var UserRole = require('../models/UserRole'); var errors = require('../errors'); var utils = require('../utils'); +/** + * Creates an RSVP and sets the users attendee role to active + * @param {Attendee} attendee the associated attendee for the rsvp + * @param {User} user the associated user for the rsvp + * @param {Object} attributes the rsvp data + * @returns {Promise} the resolved rsvp + * @throws {InvalidParameterError} thrown when an attendee already has an rsvp + */ module.exports.createRSVP = function (attendee, user, attributes) { attributes.attendeeId = attendee.get('id'); var rsvp = RSVP.forge(attributes); @@ -31,6 +39,12 @@ module.exports.createRSVP = function (attendee, user, attributes) { }) }; +/** + * Finds an RSVP by its associated attendee + * @param {Attendee} attendee the associated attendee for the rsvp + * @returns {Promise} the resolved rsvp for the attendee + * @throws {NotFoundError} when the attendee has no RSVP + */ module.exports.findRSVPByAttendee = function (attendee) { return RSVP .findByAttendeeId(attendee.get('id')) @@ -45,6 +59,12 @@ module.exports.findRSVPByAttendee = function (attendee) { }); }; +/** + * Updates a given RSVP + * @param {RSVP} rsvp the RSVP to update + * @param {Object} attributes the new RSVP data to set + * @returns {Promise} the resolved RSVP + */ module.exports.updateRSVP = function (rsvp, attributes) { rsvp.set(attributes); From cd6e832dbc8f2a7fed0b20a00d904b2516222cf8 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 19 Jan 2017 22:58:37 -0600 Subject: [PATCH 10/18] Refactored RSVP model (Postman tested, pre mocha tests) --- api/v1/controllers/RSVPController.js | 124 +++++++++--------- api/v1/models/AttendeeRSVP.js | 12 +- api/v1/requests/RSVPRequest.js | 7 +- .../services/{RSVPService => RSVPService.js} | 1 + api/v1/utils/index.js | 1 - api/v1/utils/rsvp.js | 4 +- .../V20161226_0927__createAttendees.sql | 3 +- 7 files changed, 81 insertions(+), 71 deletions(-) rename api/v1/services/{RSVPService => RSVPService.js} (98%) diff --git a/api/v1/controllers/RSVPController.js b/api/v1/controllers/RSVPController.js index a2012d8..8a08e87 100644 --- a/api/v1/controllers/RSVPController.js +++ b/api/v1/controllers/RSVPController.js @@ -1,11 +1,11 @@ var bodyParser = require('body-parser'); +var _Promise = require('bluebird'); var services = require('../services'); var middleware = require('../middleware'); var requests = require('../requests'); var roles = require('../utils/roles'); var mail = require('../utils/mail'); -var User = require('../models/User'); var router = require('express').Router(); @@ -14,30 +14,29 @@ function _isAuthenticated (req) { } function _removeFromList(rsvpCurrent, rsvpNew) { - if(rsvpCurrent.get('attendeeAttendance') !== 'NO' && rsvpNew.attendeeAttendance === 'NO') - return true; - return false; + return rsvpCurrent.get('isAttending') && !rsvpNew.isAttending; } function _addToList(rsvpCurrent, rsvpNew) { - if(rsvpCurrent.get('attendeeAttendance') === 'NO' && rsvpNew.attendeeAttendance !== 'NO') - return true; - return false; + return !rsvpCurrent.get('isAttending') && rsvpNew.isAttending; } function createRSVP(req, res, next) { + if(!req.body.isAttending) + delete req.body.type; + services.RegistrationService .findAttendeeByUser(req.user) .then(function(attendee) { - services.RSVPService - .createRSVP(attendee, req.user, req.body) - .then(function (rsvp) { - if(rsvp.get('attendeeAttendance') !== 'NO') - services.MailService.addToList(req.user, mail.lists.attendees); - res.body = rsvp.toJSON(); - - return next(); - }) + return services.RSVPService + .createRSVP(attendee, req.user, req.body); + }) + .then(function(rsvp) { + if(rsvp.get('isAttending')) + services.MailService.addToList(req.user, mail.lists.attendees); + res.body = rsvp.toJSON(); + + return next(); }) .catch(function(error) { return next(error); @@ -48,13 +47,13 @@ function fetchRSVPByUser(req, res, next) { services.RegistrationService .findAttendeeByUser(req.user) .then(function(attendee) { - services.RSVPService - .findRSVPByAttendee(attendee) - .then(function(rsvp){ - res.body = rsvp.toJSON(); + return services.RSVPService + .findRSVPByAttendee(attendee); + }) + .then(function (rsvp) { + res.body = rsvp.toJSON(); - return next(); - }) + return next(); }) .catch(function(error) { return next(error); @@ -66,12 +65,12 @@ function fetchRSVPByAttendeeId(req, res, next) { .findAttendeeById(req.params.id) .then(function(attendee) { services.RSVPService - .findRSVPByAttendee(attendee) - .then(function(rsvp){ - res.body = rsvp.toJSON(); + .findRSVPByAttendee(attendee); + }) + .then(function(rsvp){ + res.body = rsvp.toJSON(); - return next(); - }) + return next(); }) .catch(function(error) { return next(error); @@ -79,24 +78,18 @@ function fetchRSVPByAttendeeId(req, res, next) { } function updateRSVPByUser(req, res, next) { + if(!req.body.isAttending) + delete req.body.type; + services.RegistrationService .findAttendeeByUser(req.user) .then(function(attendee) { - services.RSVPService - .findRSVPByAttendee(attendee) - .then(function (rsvp) { - if(_addToList(rsvp, req.body)) - services.MailService.addToList(req.user, mail.lists.attendees); - if(_removeFromList(rsvp, req.body)) - services.MailService.removeFromList(req.user, mail.lists.attendees); - - return services.RSVPService.updateRSVP(rsvp, req.body); - }) - .then(function(rsvp){ - res.body = rsvp.toJSON(); - - return next(); - }) + return _updateRSVPByAttendee(req.user, attendee, req.body); + }) + .then(function(rsvp){ + res.body = rsvp.toJSON(); + + return next(); }) .catch(function (error) { return next(error); @@ -104,40 +97,47 @@ function updateRSVPByUser(req, res, next) { } function updateRSVPById(req, res, next) { + if(!req.body.isAttending) + delete req.body.type; + services.RegistrationService .findAttendeeById(req.params.id) .then(function(attendee) { - services.RSVPService - .findRSVPByAttendee(attendee) - .then(function (rsvp) { - if(_addToList(rsvp, req.body)) - services.MailService.addToList(req.user, mail.lists.attendees); - if(_removeFromList(rsvp, req.body)) - services.MailService.removeFromList(req.user, mail.lists.attendees); - - return services.RSVPService.updateRSVP(rsvp, req.body); - }) - .then(function(rsvp){ - res.body = rsvp.toJSON(); - - return next(); - }) + return _updateRSVPByAttendee(req.user, attendee, req.body) + }) + .then(function(rsvp){ + res.body = rsvp.toJSON(); + + return next(); }) .catch(function (error) { return next(error); }); } +function _updateRSVPByAttendee(user, attendee, newRSVP) { + return services.RSVPService + .findRSVPByAttendee(attendee) + .then(function (rsvp) { + if(_addToList(rsvp, newRSVP)) + services.MailService.addToList(user, mail.lists.attendees); + if(_removeFromList(rsvp, newRSVP)) + services.MailService.removeFromList(user, mail.lists.attendees); + + return services.RSVPService.updateRSVP(rsvp, newRSVP); + }); +} + router.use(bodyParser.json()); router.use(middleware.auth); -router.post('/', middleware.request(requests.RSVPRequest), +router.post('/attendee', middleware.request(requests.RSVPRequest), middleware.permission(roles.NONE, _isAuthenticated), createRSVP); -router.get('/', middleware.permission(roles.ATTENDEE), fetchRSVPByUser); -router.get('/:id', middleware.permission(roles.ORGANIZERS), fetchRSVPByAttendeeId); -router.put('/', middleware.request(requests.RSVPRequest), +router.get('/attendee/', middleware.permission(roles.ATTENDEE), fetchRSVPByUser); +router.get('/attendee/:id', middleware.permission(roles.ORGANIZERS), fetchRSVPByAttendeeId); +router.put('/attendee/', middleware.request(requests.RSVPRequest), middleware.permission(roles.ATTENDEE), updateRSVPByUser); -router.put('/:id', middleware.request(requests.RSVPRequest), +router.put('/attendee/:id', middleware.request(requests.RSVPRequest), middleware.permission(roles.ORGANIZERS), updateRSVPById); router.use(middleware.response); diff --git a/api/v1/models/AttendeeRSVP.js b/api/v1/models/AttendeeRSVP.js index f0e2cb9..279b162 100644 --- a/api/v1/models/AttendeeRSVP.js +++ b/api/v1/models/AttendeeRSVP.js @@ -1,4 +1,5 @@ var _ = require('lodash'); +var CheckIt = require('checkit'); var rsvp = require('../utils/rsvp'); var Model = require('./Model'); @@ -7,7 +8,7 @@ var AttendeeRSVP = Model.extend({ idAttribute: 'id', validations: { attendeeId: ['required', 'integer'], - attendeeAttendance: ['required', 'string', rsvp.verifyAttendanceReply] + isAttending: ['required', 'boolean'] } }); @@ -15,8 +16,13 @@ AttendeeRSVP.findByAttendeeId = function (attendeeId) { return AttendeeRSVP.where({ attendee_id: attendeeId }).fetch(); }; -AttendeeRSVP.findById = function (id) { - return AttendeeRSVP.where({ id: id }).fetch(); +AttendeeRSVP.prototype.validate = function () { + var checkit = CheckIt(this.validations); + checkit.maybe({type: ['required', 'string', rsvp.verifyAttendanceReply]}, function(input) { + return input.isAttending; + }); + + return checkit.run(this.attributes); }; module.exports = AttendeeRSVP; \ No newline at end of file diff --git a/api/v1/requests/RSVPRequest.js b/api/v1/requests/RSVPRequest.js index b3f0fae..1c34efa 100644 --- a/api/v1/requests/RSVPRequest.js +++ b/api/v1/requests/RSVPRequest.js @@ -1,15 +1,18 @@ var Request = require('./Request'); var rsvp = require('../utils/rsvp'); -var bodyRequired = ['attendeeAttendance']; +var bodyRequired = ['isAttending']; +var bodyAllowed = ['type']; var bodyValidations = { - 'attendeeAttendance': ['required', 'string', rsvp.verifyAttendanceReply] + 'isAttending': ['required', 'boolean'], + 'rsvpType': ['string', rsvp.verifyAttendanceReply] }; function RSVPRequest(headers, body) { Request.call(this, headers, body); this.bodyRequired = bodyRequired; + this.bodyAllowed = bodyAllowed; this.bodyValidations = bodyValidations; } diff --git a/api/v1/services/RSVPService b/api/v1/services/RSVPService.js similarity index 98% rename from api/v1/services/RSVPService rename to api/v1/services/RSVPService.js index 77253a4..ba8d20f 100644 --- a/api/v1/services/RSVPService +++ b/api/v1/services/RSVPService.js @@ -66,6 +66,7 @@ module.exports.findRSVPByAttendee = function (attendee) { * @returns {Promise} the resolved RSVP */ module.exports.updateRSVP = function (rsvp, attributes) { + rsvp.set({'type': undefined}); rsvp.set(attributes); return rsvp diff --git a/api/v1/utils/index.js b/api/v1/utils/index.js index 0fb05b3..2cf93f2 100644 --- a/api/v1/utils/index.js +++ b/api/v1/utils/index.js @@ -9,7 +9,6 @@ module.exports = { storage: require('./storage.js'), time: require('./time.js'), registration: require('./registration.js'), - rsvp: require('./rsvp.js'), validators: require('./validators.js'), cache: require("./cache.js") }; diff --git a/api/v1/utils/rsvp.js b/api/v1/utils/rsvp.js index f4e2bd0..73e9c1d 100644 --- a/api/v1/utils/rsvp.js +++ b/api/v1/utils/rsvp.js @@ -1,8 +1,8 @@ var _ = require('lodash'); -var ATTENDANCE_REPLY = ['YES', 'NO', 'YES_TO_CREATE']; +var ATTENDANCE_TYPES = ['CREATE', 'CONTRIBUTE']; module.exports.verifyAttendanceReply = function(reply){ - if (!_.includes(ATTENDANCE_REPLY, reply)) { + if (!_.includes(ATTENDANCE_TYPES, reply)) { throw new TypeError(reply + " is not a valid attendance reply option"); } diff --git a/database/migration/V20161226_0927__createAttendees.sql b/database/migration/V20161226_0927__createAttendees.sql index cdcfb15..1d8b7c0 100644 --- a/database/migration/V20161226_0927__createAttendees.sql +++ b/database/migration/V20161226_0927__createAttendees.sql @@ -119,7 +119,8 @@ CREATE TABLE `attendee_project_interests` ( CREATE TABLE `attendee_rsvps` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `attendee_id` INT UNSIGNED NOT NULL, - `attendee_attendance` ENUM('YES', 'NO', 'YES_TO_CREATE') NOT NULL, + `is_attending` TINYINT(1) NOT NULL, + `type` ENUM('CREATE', 'CONTRIBUTE'), PRIMARY KEY (`id`), INDEX `fk_attendee_rsvps_attendee_id_idx` (`attendee_id` ASC), CONSTRAINT `fk_attendee_rsvps_attendee_id` From c884bc37e27b8d7c5075c93b61f0aaeef39c5a3b Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 19 Jan 2017 23:13:37 -0600 Subject: [PATCH 11/18] Simplified get responses, fixed persistance of types --- api/v1/controllers/RSVPController.js | 6 ++++++ api/v1/services/RSVPService.js | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/api/v1/controllers/RSVPController.js b/api/v1/controllers/RSVPController.js index 8a08e87..4b65c2b 100644 --- a/api/v1/controllers/RSVPController.js +++ b/api/v1/controllers/RSVPController.js @@ -52,6 +52,9 @@ function fetchRSVPByUser(req, res, next) { }) .then(function (rsvp) { res.body = rsvp.toJSON(); + if(!res.body.type) { + delete res.body.type; + } return next(); }) @@ -69,6 +72,9 @@ function fetchRSVPByAttendeeId(req, res, next) { }) .then(function(rsvp){ res.body = rsvp.toJSON(); + if(!res.body.type) { + delete res.body.type; + } return next(); }) diff --git a/api/v1/services/RSVPService.js b/api/v1/services/RSVPService.js index ba8d20f..f385832 100644 --- a/api/v1/services/RSVPService.js +++ b/api/v1/services/RSVPService.js @@ -66,7 +66,7 @@ module.exports.findRSVPByAttendee = function (attendee) { * @returns {Promise} the resolved RSVP */ module.exports.updateRSVP = function (rsvp, attributes) { - rsvp.set({'type': undefined}); + rsvp.set({'type': null}); rsvp.set(attributes); return rsvp From eb8e8af316470c2ff1fa805e3df71cf8c8f30c48 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 19 Jan 2017 23:49:49 -0600 Subject: [PATCH 12/18] Self review fixes --- api/v1/requests/RSVPRequest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1/requests/RSVPRequest.js b/api/v1/requests/RSVPRequest.js index 1c34efa..63c4870 100644 --- a/api/v1/requests/RSVPRequest.js +++ b/api/v1/requests/RSVPRequest.js @@ -5,7 +5,7 @@ var bodyRequired = ['isAttending']; var bodyAllowed = ['type']; var bodyValidations = { 'isAttending': ['required', 'boolean'], - 'rsvpType': ['string', rsvp.verifyAttendanceReply] + 'type': ['string', rsvp.verifyAttendanceReply] }; function RSVPRequest(headers, body) { From 11be6b41294e0aef00d8fea267766a4b6df7b273 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 26 Jan 2017 20:32:00 -0600 Subject: [PATCH 13/18] Finished tests and PUTs now update AttendeeRole --- api/v1/controllers/RSVPController.js | 2 +- api/v1/services/RSVPService.js | 7 +- test/rsvp.js | 233 +++++++++++++++++++++++++++ test/test.js | 1 + 4 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 test/rsvp.js diff --git a/api/v1/controllers/RSVPController.js b/api/v1/controllers/RSVPController.js index 4b65c2b..50bf417 100644 --- a/api/v1/controllers/RSVPController.js +++ b/api/v1/controllers/RSVPController.js @@ -130,7 +130,7 @@ function _updateRSVPByAttendee(user, attendee, newRSVP) { if(_removeFromList(rsvp, newRSVP)) services.MailService.removeFromList(user, mail.lists.attendees); - return services.RSVPService.updateRSVP(rsvp, newRSVP); + return services.RSVPService.updateRSVP(user, rsvp, newRSVP); }); } diff --git a/api/v1/services/RSVPService.js b/api/v1/services/RSVPService.js index f385832..ab43fab 100644 --- a/api/v1/services/RSVPService.js +++ b/api/v1/services/RSVPService.js @@ -32,7 +32,7 @@ module.exports.createRSVP = function (attendee, user, attributes) { throw new errors.InvalidParameterError(message, source); } - var userRole = user.getRole(utils.roles.ATTENDEE) + var userRole = user.getRole(utils.roles.ATTENDEE); UserRole.setActive(userRole, true); return rsvp.save(); @@ -65,7 +65,7 @@ module.exports.findRSVPByAttendee = function (attendee) { * @param {Object} attributes the new RSVP data to set * @returns {Promise} the resolved RSVP */ -module.exports.updateRSVP = function (rsvp, attributes) { +module.exports.updateRSVP = function (user, rsvp, attributes) { rsvp.set({'type': null}); rsvp.set(attributes); @@ -73,6 +73,9 @@ module.exports.updateRSVP = function (rsvp, attributes) { .validate() .catch(CheckitError, utils.errors.handleValidationError) .then(function (validated) { + var userRole = user.getRole(utils.roles.ATTENDEE); + rsvp.get('isAttending') ? UserRole.setActive(userRole, true) : UserRole.setActive(userRole, false); + return rsvp.save(); }); }; \ No newline at end of file diff --git a/test/rsvp.js b/test/rsvp.js new file mode 100644 index 0000000..b0bbf9e --- /dev/null +++ b/test/rsvp.js @@ -0,0 +1,233 @@ +var _Promise = require('bluebird'); + +var chai = require('chai'); +var sinon = require('sinon'); + +var errors = require('../api/v1/errors'); +var utils = require('../api/v1/utils'); +var User = require('../api/v1/models/User.js'); +var Attendee = require('../api/v1/models/Attendee.js'); +var AttendeeRSVP = require('../api/v1/models/AttendeeRSVP.js'); +var RSVPService = require('../api/v1/services/RSVPService.js'); +var UserRole = require('../api/v1/models/UserRole.js'); + +var assert = chai.assert; +var expect = chai.expect; +var tracker = require('mock-knex').getTracker(); + +describe('RSVPService',function(){ + var _saveRSVP; + + describe('createRSVP', function () { + var testUser; + var testAttendee; + var testRSVP; + var _forgeRSVP; + + before(function(done){ + testUser = User.forge({ id: 1, email: 'new@example.com' }); + testUser.related('roles').add({ role: utils.roles.ATTENDEE }); + + testAttendee = Attendee.forge({ + "id": 1, + "userId": 1, + "firstName": "John", + "lastName": "Doe", + "shirtSize": "M", + "diet": "NONE", + "age": 19, + "graduationYear": 2019, + "transportation": "NOT_NEEDED", + "school": "University of Illinois at Urbana-Champaign", + "major": "Computer Science", + "gender": "MALE", + "professionalInterest": "BOTH", + "github": "JDoe1234", + "linkedin": "JDoe5678", + "interests": "CS", + "isNovice": true, + "isPrivate": false, + "hasLightningInterest": false, + "phoneNumber": "12345678910", + "ecosystemInterests": [ + { + "ecosystemId": 1, + "attendeeId": 1, + "id": 1 + } + ], + "collaborators": [ + { + "collaborator": "collaborator@hackillinois.org", + "attendeeId": 1, + "id": 1 + } + ], + "extras": [ + { + "info": "One of the projects I'm really proud of is my HelloWorld Machine. It says 'Hello World' in ten different languages!", + "attendeeId": 1, + "id": 1 + } + ] + }); + + testRSVP = { + "id": 1, + "attendeeId": 1, + "isAttending": true, + "type": "CREATE" + }; + + _forgeRSVP = sinon.spy(AttendeeRSVP, 'forge'); + _saveRSVP = sinon.spy(AttendeeRSVP.prototype, 'save'); + + done(); + }); + beforeEach(function (done) { + tracker.install(); + done(); + }); + it('creates a rsvp for a valid attendee and sets its attendee role',function(done){ + var testRSVPClone = JSON.parse(JSON.stringify(testRSVP)); + + tracker.on('query', function (query) { + query.response([]); + }); + + var RSVP = RSVPService.createRSVP(testAttendee, testUser, testRSVPClone); + RSVP.bind(this).then(function() { + var _userRole = testUser.getRole(utils.roles.ATTENDEE); + + assert(_forgeRSVP.called, "RSVP forge not called with right parameters"); + assert(_saveRSVP.calledOnce, "RSVP save not called"); + assert(_userRole.get('active'), "AttendeeRole not set to active"); + return done(); + }).catch(function (err) { + return done(err); + }); + }); + it('throws an error for an RSVP in which the type is not present when expected',function(done){ + var testRSVPClone = JSON.parse(JSON.stringify(testRSVP)); + delete testRSVPClone.type; + + var RSVP = RSVPService.createRSVP(testAttendee, testUser, testRSVPClone); + expect(RSVP).to.eventually.be.rejectedWith(errors.InvalidParameterError).and.notify(done); + }); + afterEach(function (done) { + tracker.uninstall(); + done(); + }); + after(function(done) { + _forgeRSVP.restore(); + _saveRSVP.restore(); + done(); + }); + }); + + describe('findRSVPByAttendee', function () { + var _findByAttendeeId; + var testAttendee; + var nonExistentTestAttendee; + + before(function (done) { + testAttendee = Attendee.forge({id: 1, firstName: "Example", lastName: "User"}); + nonExistentTestAttendee = Attendee.forge({id: 2, firstName: "Example", lastName: "User"}); + var testRSVP = AttendeeRSVP.forge({ + "id": 100, + "attendeeId": 1, + "isAttending": true, + "type": "CREATE" + }); + + _findByAttendeeId = sinon.stub(AttendeeRSVP,'findByAttendeeId'); + + _findByAttendeeId.withArgs(testAttendee.get('id')).returns(_Promise.resolve(testRSVP)); + _findByAttendeeId.withArgs(sinon.match.number).returns(_Promise.resolve(null)); + done(); + }); + it('finds existing rsvp',function(done){ + var RSVP = RSVPService.findRSVPByAttendee(testAttendee); + expect(RSVP).to.eventually.have.deep.property("attributes.id", 100, "ID should be 100, the searched for ID") + .then(function(){ + expect(RSVP).to.eventually.have.deep.property("attributes.isAttending", + true,"isAttending should be true").notify(done); + }); + }); + it('throws exception after searching for non-existent attendee',function(done){ + var RSVP = RSVPService.findRSVPByAttendee(nonExistentTestAttendee); + expect(RSVP).to.eventually.be.rejectedWith(errors.NotFoundError).and.notify(done); + }); + after(function(done){ + _findByAttendeeId.restore(); + done(); + }); + }); + + describe('updateRSVP', function() { + var testRSVP; + var testAttendeeRSVP; + var testUser; + var _setRSVP; + var _attendeeRole; + + before(function(done){ + testUser = User.forge({ id: 1, email: 'new@example.com' }); + testUser.related('roles').add({ role: utils.roles.ATTENDEE }); + _attendeeRole = testUser.getRole(utils.roles.ATTENDEE); + UserRole.setActive(_attendeeRole, true); + + testRSVP = { + "id": 100, + "attendeeId": 1, + "isAttending": true, + "type": "CREATE" + }; + testAttendeeRSVP = AttendeeRSVP.forge(); + + testRSVP.isAttending = false; + delete testRSVP.type; + + _setRSVP = sinon.spy(AttendeeRSVP.prototype, 'set'); + _saveRSVP = sinon.spy(AttendeeRSVP.prototype, 'save'); + + done(); + }); + beforeEach(function (done) { + tracker.install(); + done(); + }); + it('updates an RSVP',function(done){ + var testRSVPClone = JSON.parse(JSON.stringify(testRSVP)); + + tracker.on('query', function (query) { + query.response([0]); + }); + + var RSVP = RSVPService.updateRSVP(testUser, testAttendeeRSVP, testRSVPClone); + RSVP.bind(this).then(function() { + assert(_setRSVP.calledOnce, "RSVP update not called with right parameters"); + assert(_saveRSVP.calledOnce, "RSVP save not called"); + + _attendeeRole = testUser.getRole(utils.roles.ATTENDEE); + assert(!_attendeeRole.get('active'), "Role is not unset when attendance is revoked"); + assert(!this.type, "Type is not removed when attendance is revoked"); + + return done(); + }).catch(function (err) { + return done(err); + }); + + done(); + }); + afterEach(function (done) { + tracker.uninstall(); + done(); + }); + after(function(done) { + _setRSVP.restore(); + _saveRSVP.restore(); + done(); + }); + }); +}); \ No newline at end of file diff --git a/test/test.js b/test/test.js index fe9a447..467c942 100644 --- a/test/test.js +++ b/test/test.js @@ -21,5 +21,6 @@ require('./ecosystem.js'); require('./permission.js'); require('./token.js'); require('./storage.js'); +require('./rsvp.js'); mockery.disable(); From 57fa42e38c96d65da59f64722f78cd9b2c835170 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Wed, 1 Feb 2017 15:34:50 -0600 Subject: [PATCH 14/18] Code Review Changes 3 --- api/v1/controllers/RSVPController.js | 50 ++++++++-------------------- api/v1/services/RSVPService.js | 9 +++++ test/rsvp.js | 46 +++---------------------- 3 files changed, 28 insertions(+), 77 deletions(-) diff --git a/api/v1/controllers/RSVPController.js b/api/v1/controllers/RSVPController.js index 50bf417..d15b089 100644 --- a/api/v1/controllers/RSVPController.js +++ b/api/v1/controllers/RSVPController.js @@ -63,13 +63,9 @@ function fetchRSVPByUser(req, res, next) { }) } -function fetchRSVPByAttendeeId(req, res, next) { - services.RegistrationService - .findAttendeeById(req.params.id) - .then(function(attendee) { - services.RSVPService - .findRSVPByAttendee(attendee); - }) +function fetchRSVPById(req, res, next) { + services.RSVPService + .getRSVPById(req.params.id) .then(function(rsvp){ res.body = rsvp.toJSON(); if(!res.body.type) { @@ -102,35 +98,19 @@ function updateRSVPByUser(req, res, next) { }); } -function updateRSVPById(req, res, next) { - if(!req.body.isAttending) - delete req.body.type; - - services.RegistrationService - .findAttendeeById(req.params.id) - .then(function(attendee) { - return _updateRSVPByAttendee(req.user, attendee, req.body) - }) - .then(function(rsvp){ - res.body = rsvp.toJSON(); - - return next(); - }) - .catch(function (error) { - return next(error); - }); -} - function _updateRSVPByAttendee(user, attendee, newRSVP) { return services.RSVPService .findRSVPByAttendee(attendee) .then(function (rsvp) { - if(_addToList(rsvp, newRSVP)) - services.MailService.addToList(user, mail.lists.attendees); - if(_removeFromList(rsvp, newRSVP)) - services.MailService.removeFromList(user, mail.lists.attendees); - - return services.RSVPService.updateRSVP(user, rsvp, newRSVP); + return services.RSVPService.updateRSVP(user, rsvp, newRSVP) + .then(function (updatedRSVP) { + if(_addToList(rsvp, newRSVP)) + services.MailService.addToList(user, mail.lists.attendees); + if(_removeFromList(rsvp, newRSVP)) + services.MailService.removeFromList(user, mail.lists.attendees); + + return updatedRSVP + }); }); } @@ -138,13 +118,11 @@ router.use(bodyParser.json()); router.use(middleware.auth); router.post('/attendee', middleware.request(requests.RSVPRequest), - middleware.permission(roles.NONE, _isAuthenticated), createRSVP); + middleware.permission(roles.ATTENDEE, _isAuthenticated), createRSVP); router.get('/attendee/', middleware.permission(roles.ATTENDEE), fetchRSVPByUser); -router.get('/attendee/:id', middleware.permission(roles.ORGANIZERS), fetchRSVPByAttendeeId); +router.get('/attendee/:id', middleware.permission(roles.ORGANIZERS), fetchRSVPById); router.put('/attendee/', middleware.request(requests.RSVPRequest), middleware.permission(roles.ATTENDEE), updateRSVPByUser); -router.put('/attendee/:id', middleware.request(requests.RSVPRequest), - middleware.permission(roles.ORGANIZERS), updateRSVPById); router.use(middleware.response); router.use(middleware.errors); diff --git a/api/v1/services/RSVPService.js b/api/v1/services/RSVPService.js index ab43fab..b7564a7 100644 --- a/api/v1/services/RSVPService.js +++ b/api/v1/services/RSVPService.js @@ -7,6 +7,15 @@ var UserRole = require('../models/UserRole'); var errors = require('../errors'); var utils = require('../utils'); +/** + * Gets an rsvp by its id + * @param {integer} id the id of the RSVP to find + * @returns {Promise} the resolved rsvp + */ +module.exports.getRSVPById = function (id) { + return RSVP.findById(id); +}; + /** * Creates an RSVP and sets the users attendee role to active * @param {Attendee} attendee the associated attendee for the rsvp diff --git a/test/rsvp.js b/test/rsvp.js index b0bbf9e..08ed0fd 100644 --- a/test/rsvp.js +++ b/test/rsvp.js @@ -2,6 +2,7 @@ var _Promise = require('bluebird'); var chai = require('chai'); var sinon = require('sinon'); +var _ = require('lodash'); var errors = require('../api/v1/errors'); var utils = require('../api/v1/utils'); @@ -32,44 +33,7 @@ describe('RSVPService',function(){ "id": 1, "userId": 1, "firstName": "John", - "lastName": "Doe", - "shirtSize": "M", - "diet": "NONE", - "age": 19, - "graduationYear": 2019, - "transportation": "NOT_NEEDED", - "school": "University of Illinois at Urbana-Champaign", - "major": "Computer Science", - "gender": "MALE", - "professionalInterest": "BOTH", - "github": "JDoe1234", - "linkedin": "JDoe5678", - "interests": "CS", - "isNovice": true, - "isPrivate": false, - "hasLightningInterest": false, - "phoneNumber": "12345678910", - "ecosystemInterests": [ - { - "ecosystemId": 1, - "attendeeId": 1, - "id": 1 - } - ], - "collaborators": [ - { - "collaborator": "collaborator@hackillinois.org", - "attendeeId": 1, - "id": 1 - } - ], - "extras": [ - { - "info": "One of the projects I'm really proud of is my HelloWorld Machine. It says 'Hello World' in ten different languages!", - "attendeeId": 1, - "id": 1 - } - ] + "lastName": "Doe" }); testRSVP = { @@ -101,14 +65,14 @@ describe('RSVPService',function(){ assert(_forgeRSVP.called, "RSVP forge not called with right parameters"); assert(_saveRSVP.calledOnce, "RSVP save not called"); - assert(_userRole.get('active'), "AttendeeRole not set to active"); + assert(_userRole.get('active'), "Attendee role not set to active"); return done(); }).catch(function (err) { return done(err); }); }); it('throws an error for an RSVP in which the type is not present when expected',function(done){ - var testRSVPClone = JSON.parse(JSON.stringify(testRSVP)); + var testRSVPClone = _.clone(testRSVP); delete testRSVPClone.type; var RSVP = RSVPService.createRSVP(testAttendee, testUser, testRSVPClone); @@ -198,7 +162,7 @@ describe('RSVPService',function(){ done(); }); it('updates an RSVP',function(done){ - var testRSVPClone = JSON.parse(JSON.stringify(testRSVP)); + var testRSVPClone = _.clone(testRSVP); tracker.on('query', function (query) { query.response([0]); From 0c3209ed2f3cb8a1e4b1f5345bae6aead83c73a4 Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Wed, 1 Feb 2017 22:42:00 -0600 Subject: [PATCH 15/18] Cloning --- test/rsvp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/rsvp.js b/test/rsvp.js index 08ed0fd..2d8f53b 100644 --- a/test/rsvp.js +++ b/test/rsvp.js @@ -53,7 +53,7 @@ describe('RSVPService',function(){ done(); }); it('creates a rsvp for a valid attendee and sets its attendee role',function(done){ - var testRSVPClone = JSON.parse(JSON.stringify(testRSVP)); + var testRSVPClone = _.clone(testRSVP); tracker.on('query', function (query) { query.response([]); From d24f7368cf6eb212d8a7bb08be37933f905591ca Mon Sep 17 00:00:00 2001 From: Yash Sharma Date: Thu, 2 Feb 2017 16:54:22 -0600 Subject: [PATCH 16/18] Moved RSVP schema changes to their own migration --- ...V20160712_1603__createMailingListTables.sql | 4 +++- .../V20161226_0927__createAttendees.sql | 14 -------------- .../V20170202_1650__addAttendeeRSVP.sql | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 database/migration/V20170202_1650__addAttendeeRSVP.sql diff --git a/database/migration/V20160712_1603__createMailingListTables.sql b/database/migration/V20160712_1603__createMailingListTables.sql index 35fdc76..c4fb8d3 100644 --- a/database/migration/V20160712_1603__createMailingListTables.sql +++ b/database/migration/V20160712_1603__createMailingListTables.sql @@ -26,7 +26,9 @@ INSERT INTO `mailing_lists` (`name`) VALUES ('idlers'); INSERT INTO `mailing_lists` (`name`) VALUES ('applicants'); INSERT INTO `mailing_lists` (`name`) VALUES ('accepted'); INSERT INTO `mailing_lists` (`name`) VALUES ('waitlisted'); -INSERT INTO `mailing_lists` (`name`) VALUES ('attendees'); +INSERT INTO `mailing_lists` (`name`) VALUES ('software'); +INSERT INTO `mailing_lists` (`name`) VALUES ('hardware'); +INSERT INTO `mailing_lists` (`name`) VALUES ('open_source'); INSERT INTO `mailing_lists` (`name`) VALUES ('admins'); INSERT INTO `mailing_lists` (`name`) VALUES ('staff'); INSERT INTO `mailing_lists` (`name`) VALUES ('sponsors'); diff --git a/database/migration/V20161226_0927__createAttendees.sql b/database/migration/V20161226_0927__createAttendees.sql index 1d8b7c0..3d07115 100644 --- a/database/migration/V20161226_0927__createAttendees.sql +++ b/database/migration/V20161226_0927__createAttendees.sql @@ -114,18 +114,4 @@ CREATE TABLE `attendee_project_interests` ( REFERENCES `attendee_projects` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION -); - -CREATE TABLE `attendee_rsvps` ( - `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `attendee_id` INT UNSIGNED NOT NULL, - `is_attending` TINYINT(1) NOT NULL, - `type` ENUM('CREATE', 'CONTRIBUTE'), - PRIMARY KEY (`id`), - INDEX `fk_attendee_rsvps_attendee_id_idx` (`attendee_id` ASC), - CONSTRAINT `fk_attendee_rsvps_attendee_id` - FOREIGN KEY (`attendee_id`) - REFERENCES `attendees` (`id`) - ON DELETE NO ACTION - ON UPDATE NO ACTION ); \ No newline at end of file diff --git a/database/migration/V20170202_1650__addAttendeeRSVP.sql b/database/migration/V20170202_1650__addAttendeeRSVP.sql new file mode 100644 index 0000000..bb01c21 --- /dev/null +++ b/database/migration/V20170202_1650__addAttendeeRSVP.sql @@ -0,0 +1,18 @@ +CREATE TABLE `attendee_rsvps` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `attendee_id` INT UNSIGNED NOT NULL, + `is_attending` TINYINT(1) NOT NULL, + `type` ENUM('CREATE', 'CONTRIBUTE'), + PRIMARY KEY (`id`), + INDEX `fk_attendee_rsvps_attendee_id_idx` (`attendee_id` ASC), +CONSTRAINT `fk_attendee_rsvps_attendee_id` +FOREIGN KEY (`attendee_id`) +REFERENCES `attendees` (`id`) +ON DELETE NO ACTION +ON UPDATE NO ACTION +); + +INSERT INTO `mailing_lists` (`name`) VALUES ('attendees'); +DELETE FROM `mailing_lists` WHERE `name`='software'; +DELETE FROM `mailing_lists` WHERE `name`='hardware'; +DELETE FROM `mailing_lists` WHERE `name`='open_source'; \ No newline at end of file From 2313deb84c01309e307906d698348255f7348cfb Mon Sep 17 00:00:00 2001 From: Jonathan Reynolds Date: Sat, 4 Feb 2017 18:03:21 -0600 Subject: [PATCH 17/18] Fix bug with attendee updates --- api/v1/controllers/index.js | 4 ++-- api/v1/services/RegistrationService.js | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/api/v1/controllers/index.js b/api/v1/controllers/index.js index b1759de..91b1574 100644 --- a/api/v1/controllers/index.js +++ b/api/v1/controllers/index.js @@ -7,6 +7,6 @@ module.exports = { PermissionController: require('./PermissionController.js'), ProjectController: require('./ProjectController.js'), HealthController: require('./HealthController.js'), - RSVPController: require('./RSVPController.js') + RSVPController: require('./RSVPController.js'), StatsController: require('./StatsController.js') -}; \ No newline at end of file +}; diff --git a/api/v1/services/RegistrationService.js b/api/v1/services/RegistrationService.js index 3681ea3..5150a98 100644 --- a/api/v1/services/RegistrationService.js +++ b/api/v1/services/RegistrationService.js @@ -84,17 +84,19 @@ function _extractRelatedObjects(model, fkName, related) { /** * Removes unwanted objects and updates desired objects * @param {Model} model the model with which the ideas are associated + * @param {String} parentKey the foreign key name for the associated model * @param {Object} adjustments the resolved result of #_extractRelatedObjects * @param {Transaction} t a pending transaction * @return {Promise<>} a promise indicating all changes have been added to the transaction */ -function _adjustRelatedObjects(model, adjustments, t) { +function _adjustRelatedObjects(model, parentKey, adjustments, t) { var relatedPromises = []; _.forIn(adjustments, function (adjustment, relatedName) { var promise = model.related(relatedName) .query().transacting(t) .whereNotIn('id', adjustment.updatedIds) + .where(parentKey, model.get('id')) .delete() .catch(Model.NoRowsDeletedError, function () { return null; }) .then(function () { @@ -290,7 +292,7 @@ module.exports.updateMentor = function (mentor, attributes) { }) .then(function (adjustments){ return Mentor.transaction(function (t) { - return _adjustMentorIdeas(mentor, adjustments, t).then(function () { + return _adjustMentorIdeas(mentor, "mentor_id", adjustments, t).then(function () { return _saveWithRelated(mentor, { 'ideas': adjustments.ideas.new }, t); }); }); @@ -426,7 +428,7 @@ module.exports.updateAttendee = function (attendee, attributes) { }) .then(function (adjustments) { return Attendee.transaction(function (t) { - return _adjustRelatedObjects(attendee, adjustments, t) + return _adjustRelatedObjects(attendee, "attendee_id", adjustments, t) .then(function () { var newRelated = _.mapValues(adjustments, function (adjustment, adjustments) { return adjustment.new; From aeeca49ff02e8e50634bfad826d59ca371c79513 Mon Sep 17 00:00:00 2001 From: tommypacker Date: Sat, 4 Feb 2017 22:09:06 -0600 Subject: [PATCH 18/18] Format db categories --- api/v1/services/RegistrationService.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/v1/services/RegistrationService.js b/api/v1/services/RegistrationService.js index 5150a98..7eedb38 100644 --- a/api/v1/services/RegistrationService.js +++ b/api/v1/services/RegistrationService.js @@ -513,7 +513,8 @@ module.exports.findAttendeesByName = function(page, count, category, ascending, * @return {Promise} resolving to a the list of attendees */ module.exports.filterAttendees = function(page, count, category, ascending, filterCategory, filterVal) { - var ordering = (ascending ? '' : '-') + category; + var ordering = (ascending ? '' : '-') + utils.database.format(category); + filterCategory = utils.database.format(filterCategory); return Attendee .query(function (qb) { qb.where(filterCategory, '=', filterVal);