From 13a9c5c15976b1e2c532ee0d3af3004ce6a1f209 Mon Sep 17 00:00:00 2001 From: Ayelegun Kayode Michael Date: Thu, 19 Apr 2018 14:53:49 +0100 Subject: [PATCH] feature/modify-db - add startTime column - add endTime column to events database - write controller to ensure booked times are not overlapping - write tests for controller --- package-lock.json | 70 +++++++++++++++-- package.json | 5 +- server/controllers/event.js | 75 +++++++++++++++---- server/migrations/20180418225056-startTime.js | 26 +++++++ server/migrations/20180418225209-endTime.js | 26 +++++++ .../migrations/20180418225803-dropDuration.js | 24 ++++++ .../migrations/20180418225851-addDuration.js | 26 +++++++ .../20180418230549-dropDuration2.js | 24 ++++++ .../migrations/20180418230619-addDuration2.js | 26 +++++++ server/models/event.js | 6 ++ server/seeders/20180418211657-main-event.js | 12 ++- server/tests/event.test.js | 34 ++++----- server/tests/seed/eventSeed.js | 11 ++- server/validators/validateAddEvent.js | 3 - 14 files changed, 318 insertions(+), 50 deletions(-) create mode 100644 server/migrations/20180418225056-startTime.js create mode 100644 server/migrations/20180418225209-endTime.js create mode 100644 server/migrations/20180418225803-dropDuration.js create mode 100644 server/migrations/20180418225851-addDuration.js create mode 100644 server/migrations/20180418230549-dropDuration2.js create mode 100644 server/migrations/20180418230619-addDuration2.js diff --git a/package-lock.json b/package-lock.json index 7b5458b..86a3ac9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1647,6 +1647,14 @@ "cssom": "0.3.2" } }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "requires": { + "es5-ext": "0.10.42" + } + }, "damerau-levenshtein": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz", @@ -1967,6 +1975,35 @@ "is-symbol": "1.0.1" } }, + "es5-ext": { + "version": "0.10.42", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.42.tgz", + "integrity": "sha512-AJxO1rmPe1bDEfSR6TJ/FgMFYuTBhR5R57KW58iCkYACMyFbrkqVyzXSurYoScDGvgyMpk7uRF/lPUPPTmsRSA==", + "requires": { + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1", + "next-tick": "1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.42", + "es6-symbol": "3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.42" + } + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -4822,7 +4859,7 @@ "requires": { "hoek": "2.16.3", "isemail": "1.2.0", - "moment": "2.22.0", + "moment": "2.22.1", "topo": "1.1.0" }, "dependencies": { @@ -5423,16 +5460,24 @@ "dev": true }, "moment": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.0.tgz", - "integrity": "sha512-1muXCh8jb1N/gHRbn9VDUBr0GYb8A/aVcHlII9QSB68a50spqEVLIGN6KVmCOnSvJrUhC0edGgKU5ofnGXdYdg==" + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", + "integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==" + }, + "moment-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/moment-range/-/moment-range-4.0.0.tgz", + "integrity": "sha1-9g7VbMfh+wzeXQbrjadgImxTkqE=", + "requires": { + "es6-symbol": "3.1.1" + } }, "moment-timezone": { "version": "0.5.14", "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.14.tgz", "integrity": "sha1-TrOP+VOLgBCLpGekWPPtQmjM/LE=", "requires": { - "moment": "2.22.0" + "moment": "2.22.1" } }, "morgan": { @@ -5542,6 +5587,11 @@ } } }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, "nocache": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz", @@ -10143,7 +10193,7 @@ "generic-pool": "3.4.2", "inflection": "1.12.0", "lodash": "4.17.5", - "moment": "2.22.0", + "moment": "2.22.1", "moment-timezone": "0.5.14", "retry-as-promised": "2.3.2", "semver": "5.5.0", @@ -10999,6 +11049,14 @@ "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", "dev": true }, + "timediff": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/timediff/-/timediff-1.1.1.tgz", + "integrity": "sha1-ZkN6+bCUAicR596FDssAqfNAfTg=", + "requires": { + "moment": "2.22.1" + } + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", diff --git a/package.json b/package.json index 877bf98..47644e7 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,15 @@ "jsonwebtoken": "^8.2.1", "libphonenumber-js": "^1.1.10", "lodash": "^4.17.5", + "moment": "^2.22.1", + "moment-range": "^4.0.0", "morgan": "^1.9.0", "nexmo": "^2.2.0", "nodemailer": "^4.6.4", "pg": "^7.4.1", "pg-hstore": "^2.3.2", - "sequelize": "^4.37.6" + "sequelize": "^4.37.6", + "timediff": "^1.1.1" }, "devDependencies": { "chai": "^4.1.2", diff --git a/server/controllers/event.js b/server/controllers/event.js index 3897cda..f2bf1fc 100644 --- a/server/controllers/event.js +++ b/server/controllers/event.js @@ -1,3 +1,6 @@ +import Moment from 'moment'; +import { extendMoment } from 'moment-range'; +import timeDifference from 'timediff'; import db from '../models'; import mailer from './mailer'; import serverError from '../errorHandler/serverError'; @@ -21,23 +24,62 @@ class Events { message: 'No center found with this name.' }); } - Event.create({ - name: request.body.name, - image: request.body.image, - date: request.body.date, - duration: request.body.duration, - organizer: request.userDetails.id, - venue: foundCenter.dataValues.id - }).then((event) => { - return response.status(201).json({ - message: 'Event created successfully.', - eventDetails: { - name: event.name, - image: event.image, - date: event.date, - duration: event.duration, - venue: foundCenter.dataValues.name + const duration = timeDifference( + request.body.startTime, + request.body.endTime + ); + + const convertStartTime = new Date(request.body.startTime) + .toLocaleTimeString('en-US', { hour12: false }); + const convertEndTime = new Date(request.body.endTime) + .toLocaleTimeString('en-US', { hour12: false }); + + const eventDuration = + `${duration.hours}hour(s), ${duration.minutes}minutes`; + + return Event.findAll({ + where: { + date: request.body.date, + venue: foundCenter.dataValues.id + } + }).then((foundEvent) => { + for (let index = 0; index < foundEvent.length; index += 1) { + const event = foundEvent[index]; + if ((convertStartTime >= event.dataValues.startTime + && convertStartTime <= event.dataValues.endTime) || + (convertEndTime <= event.dataValues.endTime + && convertEndTime >= event.dataValues.startTime)) { + return response.status(409).json({ + message: + `Sorry, you cannot book an event at this + time because there will be an event + holding between ${event.dataValues.startTime} and ${event.dataValues.endTime}.` + }); } + } + + return Event.create({ + name: request.body.name, + image: request.body.image, + date: request.body.date, + startTime: request.body.startTime, + endTime: request.body.endTime, + duration: eventDuration, + organizer: request.userDetails.id, + venue: foundCenter.dataValues.id + }).then((event) => { + return response.status(201).json({ + message: 'Event created successfully.', + eventDetails: { + name: event.name, + image: event.image, + date: event.date, + startTime: event.startTime, + endTime: event.endTime, + duration: event.duration, + venue: foundCenter.dataValues.name + } + }); }); }); }).catch((err) => { @@ -46,6 +88,7 @@ class Events { }); }); } + static getEvents(request, response) { return Event.findAndCountAll() .then((allEvents) => { diff --git a/server/migrations/20180418225056-startTime.js b/server/migrations/20180418225056-startTime.js new file mode 100644 index 0000000..ecbe2ee --- /dev/null +++ b/server/migrations/20180418225056-startTime.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + return queryInterface.addColumn('Events', 'startTime', { + type: Sequelize.TIME + }); + }, + + down: (queryInterface, Sequelize) => { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.dropTable('users'); + */ + } +}; diff --git a/server/migrations/20180418225209-endTime.js b/server/migrations/20180418225209-endTime.js new file mode 100644 index 0000000..23f43c5 --- /dev/null +++ b/server/migrations/20180418225209-endTime.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + return queryInterface.addColumn('Events', 'endTime', { + type: Sequelize.TIME + }); + }, + + down: (queryInterface, Sequelize) => { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.dropTable('users'); + */ + } +}; diff --git a/server/migrations/20180418225803-dropDuration.js b/server/migrations/20180418225803-dropDuration.js new file mode 100644 index 0000000..27e3a07 --- /dev/null +++ b/server/migrations/20180418225803-dropDuration.js @@ -0,0 +1,24 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + return queryInterface.removeColumn('Events', 'duration'); + }, + + down: (queryInterface, Sequelize) => { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.dropTable('users'); + */ + } +}; diff --git a/server/migrations/20180418225851-addDuration.js b/server/migrations/20180418225851-addDuration.js new file mode 100644 index 0000000..48fb92b --- /dev/null +++ b/server/migrations/20180418225851-addDuration.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + return queryInterface.addColumn('Events', 'duration', { + type: Sequelize.TIME + }); + }, + + down: (queryInterface, Sequelize) => { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.dropTable('users'); + */ + } +}; diff --git a/server/migrations/20180418230549-dropDuration2.js b/server/migrations/20180418230549-dropDuration2.js new file mode 100644 index 0000000..27e3a07 --- /dev/null +++ b/server/migrations/20180418230549-dropDuration2.js @@ -0,0 +1,24 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + return queryInterface.removeColumn('Events', 'duration'); + }, + + down: (queryInterface, Sequelize) => { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.dropTable('users'); + */ + } +}; diff --git a/server/migrations/20180418230619-addDuration2.js b/server/migrations/20180418230619-addDuration2.js new file mode 100644 index 0000000..8a7348b --- /dev/null +++ b/server/migrations/20180418230619-addDuration2.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + return queryInterface.addColumn('Events', 'duration', { + type: Sequelize.TEXT + }); + }, + + down: (queryInterface, Sequelize) => { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.dropTable('users'); + */ + } +}; diff --git a/server/models/event.js b/server/models/event.js index 479d3dd..b073292 100644 --- a/server/models/event.js +++ b/server/models/event.js @@ -16,6 +16,12 @@ module.exports = (sequelize, DataTypes) => { allowNull: false, type: DataTypes.STRING }, + startTime: { + type: DataTypes.TIME + }, + endTime: { + type: DataTypes.TIME + } }); Event.associate = (models) => { Event.belongsTo(models.Center, { diff --git a/server/seeders/20180418211657-main-event.js b/server/seeders/20180418211657-main-event.js index f7cbe3d..d6c673c 100644 --- a/server/seeders/20180418211657-main-event.js +++ b/server/seeders/20180418211657-main-event.js @@ -15,7 +15,9 @@ module.exports = { return queryInterface.bulkInsert('Events', [ { name: 'Biker sports event', - duration: 2, + startTime: '19:00:00', + endTime: '20:00:00', + duration: '1hour(s), 00minutes', date: '2018-03-05 12:01:18.936+01', organizer: 1, venue: 1, @@ -24,7 +26,9 @@ module.exports = { }, { name: 'NBA Playoffs', - duration: 2, + startTime: '17:00:00', + endTime: '18:00:00', + duration: '1hour(s), 00minutes', date: '2018-03-05 12:01:18.936+01', organizer: 1, venue: 1, @@ -33,7 +37,9 @@ module.exports = { }, { name: 'Paralympics', - duration: 2, + startTime: '10:00:00', + endTime: '15:00:00', + duration: '5hour(s), 00minutes', date: '2018-03-05 12:01:18.936+01', organizer: 1, venue: 1, diff --git a/server/tests/event.test.js b/server/tests/event.test.js index f381e09..26da1b2 100644 --- a/server/tests/event.test.js +++ b/server/tests/event.test.js @@ -1,7 +1,7 @@ import supertest from 'supertest'; import { expect } from 'chai'; import app from '../app'; -import eventSeed from './seed/eventSeed'; +import eventSeed, { secondEvent } from './seed/eventSeed'; import dummyUser, { secondDummyUser } from './seed/userseed'; const request = supertest(app); @@ -61,22 +61,6 @@ describe('Tests for Events API', () => { done(); }); }); - it('should fail to create an event if no duration is supplied', (done) => { - const noDuration = { ...eventSeed }; - noDuration.duration = ''; - request.post(`/api/v1/events?token=${secondDummyUser.token}`) - .set('Connection', 'keep alive') - .set('Content-Type', 'application/json') - .type('form') - .send(noDuration) - .end((error, response) => { - expect(response.body).to.be.an('object'); - expect(response.body.duration).to - .equal('Please enter a duration for your event'); - expect(response.status).to.equal(400); - done(); - }); - }); it( 'should fail to create an event if the center does not exist', (done) => { @@ -127,6 +111,22 @@ describe('Tests for Events API', () => { }); } ); + it('should fail to create an event if the time slot is already taken', (done) => { + request.post(`/api/v1/events?token=${secondDummyUser.token}`) + .set('Connection', 'keep alive') + .set('Content-Type', 'application/json') + .type('form') + .send(secondEvent) + .end((error, response) => { + expect(response.status).to.equal(409); + expect(response.body.message).to.equal( + `Sorry, you cannot book an event at this + time because there will be an event + holding between 05:00:00 and 07:00:00.` + ); + done(); + }); + }); }); describe('Get all events test', () => { it('should fetch all the events in the application', (done) => { diff --git a/server/tests/seed/eventSeed.js b/server/tests/seed/eventSeed.js index 3680a3e..f175884 100644 --- a/server/tests/seed/eventSeed.js +++ b/server/tests/seed/eventSeed.js @@ -1,16 +1,19 @@ const eventSeed = { id: 1, name: 'Biker sports event', - duration: 2, + startTime: '2018-04-19 05:00:00 +01:00', + endTime: '2018-04-19 07:00:00 +01:00', date: '2018-03-05 12:01:18.936+01', venue: 'Yaba Beach' }; export const secondEvent = { - id: 2, - name: 'Dripping migos', - duration: '2018-03-05 12:01:18.936+01', + name: 'Danger boy school', + startTime: '2018-04-19 05:00:00 +01:00', + endTime: '2018-04-19 07:00:00 +01:00', + date: '2018-03-05 12:01:18.936+01', venue: 'Yaba Beach' }; + export default eventSeed; diff --git a/server/validators/validateAddEvent.js b/server/validators/validateAddEvent.js index be2690b..72ee63e 100644 --- a/server/validators/validateAddEvent.js +++ b/server/validators/validateAddEvent.js @@ -8,9 +8,6 @@ const validateAddEvent = (eventData) => { if (eventData.date.trim() === '' || eventData.date === undefined) { errors.date = 'Please input a date for your event'; } - if (eventData.duration.trim() === '' || eventData.duration === undefined) { - errors.duration = 'Please enter a duration for your event'; - } return { errors,