From 9a02d96a7fc8ebd88016feb3abb3fa54ca609c47 Mon Sep 17 00:00:00 2001 From: Pierre Theo Klein Date: Sun, 16 Dec 2018 22:12:34 -0500 Subject: [PATCH 1/3] Write stats route --- constants/role.constant.js | 2 +- constants/routes.constant.js | 10 +++++ controllers/hacker.controller.js | 11 ++++++ middlewares/hacker.middleware.js | 7 ++++ models/account.model.js | 13 ++++++- package-lock.json | 9 ++++- package.json | 3 +- routes/api/hacker.js | 39 +++++++++++++++++++ services/hacker.service.js | 67 ++++++++++++++++++++++++++++++-- 9 files changed, 152 insertions(+), 9 deletions(-) diff --git a/constants/role.constant.js b/constants/role.constant.js index 8b2f7870..0d0d8dec 100644 --- a/constants/role.constant.js +++ b/constants/role.constant.js @@ -6,7 +6,7 @@ const Constants = { const mongoose = require("mongoose"); const accountRole = { - "_id": mongoose.Types.ObjectId(0), + "_id": mongoose.Types.ObjectId.createFromTime(0), "name": "account", "routes": [ Constants.Routes.authRoutes.login, diff --git a/constants/routes.constant.js b/constants/routes.constant.js index 30504705..64f63bfe 100644 --- a/constants/routes.constant.js +++ b/constants/routes.constant.js @@ -159,6 +159,14 @@ const volunteerRoutes = { }, }; + +const staffRoutes = { + "hackerStats": { + requestType: Constants.REQUEST_TYPES.GET, + uri: "/api/hacker/stats", + } +} + const allRoutes = { "Auth": authRoutes, "Account": accountRoutes, @@ -166,6 +174,7 @@ const allRoutes = { "Sponsor": sponsorRoutes, "Team": teamRoutes, "Volunteer": volunteerRoutes, + "Staff": staffRoutes, }; /** @@ -201,6 +210,7 @@ module.exports = { sponsorRoutes: sponsorRoutes, teamRoutes: teamRoutes, volunteerRoutes: volunteerRoutes, + staffRoutes: staffRoutes, allRoutes: allRoutes, listAllRoutes: listAllRoutes, }; \ No newline at end of file diff --git a/controllers/hacker.controller.js b/controllers/hacker.controller.js index 3c292a65..dd5164a1 100644 --- a/controllers/hacker.controller.js +++ b/controllers/hacker.controller.js @@ -97,6 +97,16 @@ function downloadedResume(req, res) { }); } +function gotStats(req, res) { + return res.status(200).json({ + message: "Retrieved stats", + data: { + stats: req.body.stats, + } + }); + +} + module.exports = { updatedHacker: updatedHacker, findById: Util.asyncMiddleware(findById), @@ -104,4 +114,5 @@ module.exports = { uploadedResume: uploadedResume, downloadedResume: downloadedResume, showHacker: showHacker, + gotStats: gotStats, }; \ No newline at end of file diff --git a/middlewares/hacker.middleware.js b/middlewares/hacker.middleware.js index 88d25935..67064621 100644 --- a/middlewares/hacker.middleware.js +++ b/middlewares/hacker.middleware.js @@ -462,6 +462,12 @@ async function findSelf(req, res, next) { } } +async function getStats(req, res, next) { + const stats = await Services.Hacker.getStats(); + req.body.stats = stats; + next(); +} + module.exports = { parsePatch: parsePatch, parseHacker: parseHacker, @@ -480,4 +486,5 @@ module.exports = { parseConfirmation: parseConfirmation, createHacker: Middleware.Util.asyncMiddleware(createHacker), findSelf: Middleware.Util.asyncMiddleware(findSelf), + getStats: Middleware.Util.asyncMiddleware(getStats) }; \ No newline at end of file diff --git a/models/account.model.js b/models/account.model.js index ff3c3401..412bc110 100644 --- a/models/account.model.js +++ b/models/account.model.js @@ -79,8 +79,17 @@ AccountSchema.methods.comparePassword = function (password) { /** * Returns if the accountType corresponds to a sponsor */ -AccountSchema.methods.isSponsor = function(){ +AccountSchema.methods.isSponsor = function () { return Constants.SPONSOR_TIERS.includes(this.accountType) || this.accountType == Constants.SPONSOR; -} +}; +/** + * Calculates the user's age + */ +AccountSchema.methods.getAge = function () { // birthday is a date + var ageDifMs = Date.now() - this.birthDate.getTime(); + var ageDate = new Date(ageDifMs); // miliseconds from epoch + return Math.abs(ageDate.getUTCFullYear() - 1970); +}; + //export the model module.exports = mongoose.model("Account", AccountSchema); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3e93231e..9ac919e8 100755 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "dependencies": { "@google-cloud/common": { "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.17.0.tgz", + "resolved": "http://registry.npmjs.org/@google-cloud/common/-/common-0.17.0.tgz", "integrity": "sha512-HRZLSU762E6HaKoGfJGa8W95yRjb9rY7LePhjaHK9ILAnFacMuUGVamDbTHu1csZomm1g3tZTtXfX/aAhtie/Q==", "requires": { "array-uniq": "^1.0.3", @@ -3429,7 +3429,7 @@ }, "gcp-metadata": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.6.3.tgz", + "resolved": "http://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.6.3.tgz", "integrity": "sha512-MSmczZctbz91AxCvqp9GHBoZOSbJKAICV7Ow/AIWSJZRrRchUd5NL1b2P4OfP+4m490BEUPhhARfpHdqCxuCvg==", "requires": { "axios": "^0.18.0", @@ -5237,6 +5237,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "memory-cache": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz", + "integrity": "sha1-eJCwHVLADI68nVM+H46xfjA0hxo=" + }, "meow": { "version": "3.7.0", "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", diff --git a/package.json b/package.json index 92aee488..6905b136 100755 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "express-winston": "^2.5.1", "handlebars": "^4.0.12", "jsonwebtoken": "^8.1.0", + "memory-cache": "^0.2.0", "mongoose": "^5.1.0", "multer": "^1.3.1", "passport": "^0.4.0", @@ -47,4 +48,4 @@ "mocha": "^5.2.0", "nodemon": "^1.17.3" } -} \ No newline at end of file +} diff --git a/routes/api/hacker.js b/routes/api/hacker.js index 2282aba8..b75d291f 100644 --- a/routes/api/hacker.js +++ b/routes/api/hacker.js @@ -166,6 +166,45 @@ module.exports = { Middleware.Hacker.sendAppliedStatusEmail, Controllers.Hacker.createdHacker ); + + /** + * @api {get} /hacker/stats + * Gets the stats of all of the hackers who have applied. + * @apiName getHackerStats + * @apiGroup Hacker + * @apiVersion 0.0.9 + * @apiSuccess {string} message Success message + * @apiSuccess {object} data Hacker object + * @apiSuccessExample {object} Success-Response: + * { + * "message": "Retrieved stats", + * "data": { + * "stats" : { + * "total": 10, + "status": { "Applied": 10 }, + "school": { "McGill University": 3, "Harvard University": 7 }, + degree: { "Undergraduate": 10 }, + gender: { "Male": 1, "Female": 9 }, + needsBus: { "true": 7, "false": 3 }, + ethnicity: { "White": 10, }, + jobInterest: { "Internship": 10 }, + major: { "Computer Science": 10 }, + graduationYear: { "2019": 10 }, + dietaryRestrictions: { "None": 10 }, + shirtSize: { "M": 3, "XL": 7 }, + age: { "22": 10 } + } + * } + * } + * + */ + hackerRouter.route("/stats").get( + Middleware.Auth.ensureAuthenticated(), + Middleware.Auth.ensureAuthorized(), + Middleware.Hacker.getStats, + Controllers.Hacker.gotStats + ); + /** * @api {patch} /hacker/status/:id update a hacker's status * @apiName patchHackerStatus diff --git a/services/hacker.service.js b/services/hacker.service.js index f732b36a..0b2b3a46 100644 --- a/services/hacker.service.js +++ b/services/hacker.service.js @@ -2,6 +2,9 @@ const Hacker = require("../models/hacker.model"); const logger = require("./logger.service"); +const cache = require("memory-cache"); + +const Constants = require("../constants/general.constant"); /** * @function createHacker * @param {{_id: ObjectId, accountId: ObjectId, school: string, gender: string, needsBus: boolean, application: {Object}}} hackerDetails @@ -56,11 +59,10 @@ async function findIds(queries) { const TAG = `[Hacker Service # findIds ]:`; let ids = []; - queries.forEach(async (query) => { + for (const query of queries) { let currId = await Hacker.findOne(query, "_id", logger.queryCallbackFactory(TAG, "hacker", query)); ids.push(currId); - }); - + } return ids; } @@ -79,10 +81,69 @@ function findByAccountId(accountId) { return Hacker.findOne(query, logger.updateCallbackFactory(TAG, "hacker")); } +async function getStats() { + const TAG = `[ hacker Service # getHackerStats ]`; + const CACHE_KEY = "hackerStats"; + if (cache.get(CACHE_KEY) !== null) { + logger.info(`${TAG} Getting cached stats`); + return cache.get(CACHE_KEY); + } + const allHackers = await Hacker.find({}, logger.updateCallbackFactory(TAG, "hacker")).populate({ + path: "accountId", + }); + const stats = { + total: 0, + status: {}, + school: {}, + degree: {}, + gender: {}, + needsBus: {}, + ethnicity: {}, + jobInterest: {}, + major: {}, + graduationYear: {}, + dietaryRestrictions: {}, + shirtSize: {}, + age: {} + }; + + allHackers.forEach((hacker) => { + if (!hacker.accountId) { + // user is no longer with us for some reason :( + return; + } + stats.total += 1; + stats.status[hacker.status] = (stats.status[hacker.status]) ? stats.status[hacker.status] + 1 : 1; + stats.school[hacker.school] = (stats.school[hacker.school]) ? stats.school[hacker.school] + 1 : 1; + stats.degree[hacker.degree] = (stats.degree[hacker.degree]) ? stats.degree[hacker.degree] + 1 : 1; + stats.gender[hacker.gender] = (stats.gender[hacker.gender]) ? stats.gender[hacker.gender] + 1 : 1; + stats.needsBus[hacker.needsBus] = (stats.needsBus[hacker.needsBus]) ? stats.needsBus[hacker.needsBus] + 1 : 1; + + for (const ethnicity of hacker.ethnicity) { + stats.ethnicity[ethnicity] = (stats.ethnicity[ethnicity]) ? stats.ethnicity[ethnicity] + 1 : 1; + } + + stats.jobInterest[hacker.application.jobInterest] = (stats.jobInterest[hacker.application.jobInterest]) ? stats.jobInterest[hacker.application.jobInterest] + 1 : 1; + stats.major[hacker.major] = (stats.major[hacker.major]) ? stats.major[hacker.major] + 1 : 1; + stats.graduationYear[hacker.graduationYear] = (stats.graduationYear[hacker.graduationYear]) ? stats.graduationYear[hacker.graduationYear] + 1 : 1; + + for (const dietaryRestrictions of hacker.accountId.dietaryRestrictions) { + stats.dietaryRestrictions[dietaryRestrictions] = (stats.dietaryRestrictions[dietaryRestrictions]) ? stats.dietaryRestrictions[dietaryRestrictions] + 1 : 1; + } + stats.shirtSize[hacker.accountId.shirtSize] = (stats.shirtSize[hacker.accountId.shirtSize]) ? stats.shirtSize[hacker.accountId.shirtSize] + 1 : 1; + const age = hacker.accountId.getAge(); + stats.age[age] = (stats.age[age]) ? stats.age[age] + 1 : 1; + }); + cache.put(CACHE_KEY, stats, 5 * 60 * 1000); //set a time-out of 5 minutes + return stats; +} + + module.exports = { createHacker: createHacker, findById: findById, updateOne: updateOne, findIds: findIds, findByAccountId: findByAccountId, + getStats: getStats }; \ No newline at end of file From 7de707b241f39d2dbf9a63350b52cf1f838ffa0e Mon Sep 17 00:00:00 2001 From: Pierre Theo Klein Date: Sun, 16 Dec 2018 22:13:56 -0500 Subject: [PATCH 2/3] Update docs --- docs/api/api_data.js | 37 +++++++++++++++++++++++++++++++++++++ docs/api/api_data.json | 37 +++++++++++++++++++++++++++++++++++++ docs/api/api_project.js | 2 +- docs/api/api_project.json | 2 +- 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/docs/api/api_data.js b/docs/api/api_data.js index 4f5597f3..92dd875f 100644 --- a/docs/api/api_data.js +++ b/docs/api/api_data.js @@ -1355,6 +1355,43 @@ define({ "filename": "routes/api/hacker.js", "groupTitle": "Hacker" }, + { + "type": "get", + "url": "/hacker/stats", + "title": "Gets the stats of all of the hackers who have applied.", + "name": "getHackerStats", + "group": "Hacker", + "version": "0.0.9", + "success": { + "fields": { + "Success 200": [{ + "group": "Success 200", + "type": "string", + "optional": false, + "field": "message", + "description": "

Success message

" + }, + { + "group": "Success 200", + "type": "object", + "optional": false, + "field": "data", + "description": "

Hacker object

" + } + ] + }, + "examples": [{ + "title": "Success-Response: ", + "content": "{\n \"message\": \"Retrieved stats\",\n \"data\": {\n \"stats\" : {\n \"total\": 10,\n \"status\": { \"Applied\": 10 },\n \"school\": { \"McGill University\": 3, \"Harvard University\": 7 },\n degree: { \"Undergraduate\": 10 },\n gender: { \"Male\": 1, \"Female\": 9 },\n needsBus: { \"true\": 7, \"false\": 3 },\n ethnicity: { \"White\": 10, },\n jobInterest: { \"Internship\": 10 },\n major: { \"Computer Science\": 10 },\n graduationYear: { \"2019\": 10 },\n dietaryRestrictions: { \"None\": 10 },\n shirtSize: { \"M\": 3, \"XL\": 7 },\n age: { \"22\": 10 }\n }\n }\n}", + "type": "object" + }] + }, + "filename": "routes/api/hacker.js", + "groupTitle": "Hacker", + "sampleRequest": [{ + "url": "https://api.mchacks.ca/api/hacker/stats" + }] + }, { "type": "patch", "url": "/hacker/:id", diff --git a/docs/api/api_data.json b/docs/api/api_data.json index 933b1514..b42ad911 100644 --- a/docs/api/api_data.json +++ b/docs/api/api_data.json @@ -1354,6 +1354,43 @@ "filename": "routes/api/hacker.js", "groupTitle": "Hacker" }, + { + "type": "get", + "url": "/hacker/stats", + "title": "Gets the stats of all of the hackers who have applied.", + "name": "getHackerStats", + "group": "Hacker", + "version": "0.0.9", + "success": { + "fields": { + "Success 200": [{ + "group": "Success 200", + "type": "string", + "optional": false, + "field": "message", + "description": "

Success message

" + }, + { + "group": "Success 200", + "type": "object", + "optional": false, + "field": "data", + "description": "

Hacker object

" + } + ] + }, + "examples": [{ + "title": "Success-Response: ", + "content": "{\n \"message\": \"Retrieved stats\",\n \"data\": {\n \"stats\" : {\n \"total\": 10,\n \"status\": { \"Applied\": 10 },\n \"school\": { \"McGill University\": 3, \"Harvard University\": 7 },\n degree: { \"Undergraduate\": 10 },\n gender: { \"Male\": 1, \"Female\": 9 },\n needsBus: { \"true\": 7, \"false\": 3 },\n ethnicity: { \"White\": 10, },\n jobInterest: { \"Internship\": 10 },\n major: { \"Computer Science\": 10 },\n graduationYear: { \"2019\": 10 },\n dietaryRestrictions: { \"None\": 10 },\n shirtSize: { \"M\": 3, \"XL\": 7 },\n age: { \"22\": 10 }\n }\n }\n}", + "type": "object" + }] + }, + "filename": "routes/api/hacker.js", + "groupTitle": "Hacker", + "sampleRequest": [{ + "url": "https://api.mchacks.ca/api/hacker/stats" + }] + }, { "type": "patch", "url": "/hacker/:id", diff --git a/docs/api/api_project.js b/docs/api/api_project.js index 62fd88fb..6d90c460 100644 --- a/docs/api/api_project.js +++ b/docs/api/api_project.js @@ -9,7 +9,7 @@ define({ "apidoc": "0.3.0", "generator": { "name": "apidoc", - "time": "2018-12-11T06:22:15.249Z", + "time": "2018-12-17T03:13:26.391Z", "url": "http://apidocjs.com", "version": "0.17.6" } diff --git a/docs/api/api_project.json b/docs/api/api_project.json index 8ae30989..ff163a2c 100644 --- a/docs/api/api_project.json +++ b/docs/api/api_project.json @@ -9,7 +9,7 @@ "apidoc": "0.3.0", "generator": { "name": "apidoc", - "time": "2018-12-11T06:22:15.249Z", + "time": "2018-12-17T03:13:26.391Z", "url": "http://apidocjs.com", "version": "0.17.6" } From 6d92d06426974ec7ab6163ddca2a44ed2bf2a192 Mon Sep 17 00:00:00 2001 From: Pierre Theo Klein Date: Sun, 16 Dec 2018 22:30:23 -0500 Subject: [PATCH 3/3] Add tests --- tests/hacker.test.js | 65 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/hacker.test.js b/tests/hacker.test.js index 8b18025d..6cea4d30 100644 --- a/tests/hacker.test.js +++ b/tests/hacker.test.js @@ -719,4 +719,69 @@ describe("POST add a hacker resume", function () { }); }); }); +}); + +describe("GET Hacker stats", function () { + it("It should SUCCEED and get hacker stats", function (done) { + //this takes a lot of time for some reason + util.auth.login(agent, Admin1, (error) => { + if (error) { + return done(error); + } + return agent + .get(`/api/hacker/stats`) + .end(function (err, res) { + res.should.have.status(200); + res.should.have.property("body"); + res.body.should.have.property("message"); + res.body.message.should.equal("Retrieved stats"); + res.body.should.have.property("data"); + res.body.data.should.have.property("stats"); + res.body.data.stats.should.have.property("total"); + res.body.data.stats.should.have.property("status"); + res.body.data.stats.should.have.property("school"); + res.body.data.stats.should.have.property("degree"); + res.body.data.stats.should.have.property("gender"); + res.body.data.stats.should.have.property("needsBus"); + res.body.data.stats.should.have.property("ethnicity"); + res.body.data.stats.should.have.property("jobInterest"); + res.body.data.stats.should.have.property("major"); + res.body.data.stats.should.have.property("graduationYear"); + res.body.data.stats.should.have.property("dietaryRestrictions"); + res.body.data.stats.should.have.property("shirtSize"); + res.body.data.stats.should.have.property("age"); + done(); + }); + }); + }); + it("It should FAIL and get hacker stats due to invalid Authorization", function (done) { + //this takes a lot of time for some reason + util.auth.login(agent, storedAccount1, (error) => { + if (error) { + return done(error); + } + return agent + .get(`/api/hacker/stats`) + .end(function (err, res) { + res.should.have.status(403); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal(Constants.Error.AUTH_403_MESSAGE); + res.body.should.have.property("data"); + done(); + }); + }); + }); + it("It should FAIL and get hacker stats due to invalid Authentication", function (done) { + //this takes a lot of time for some reason + chai.request(server.app) + .get(`/api/hacker/stats`) + .end(function (err, res) { + res.should.have.status(401); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal(Constants.Error.AUTH_401_MESSAGE); + done(); + }); + }); }); \ No newline at end of file