From e610527ddc018f26022e10cac6f31e7ea9a2536e Mon Sep 17 00:00:00 2001 From: Thomas Kranitsas Date: Thu, 9 Dec 2021 18:44:46 +0200 Subject: [PATCH] add endpoint for challenge statistics --- config/default.js | 3 ++ src/common/helper.js | 48 +++++++++++++++++++++++++- src/controllers/ChallengeController.js | 13 ++++++- src/routes.js | 6 ++++ src/services/ChallengeService.js | 46 +++++++++++++++++++++++- 5 files changed, 113 insertions(+), 3 deletions(-) diff --git a/config/default.js b/config/default.js index f34e8129..1831f2af 100644 --- a/config/default.js +++ b/config/default.js @@ -48,6 +48,9 @@ module.exports = { // in bytes FILE_UPLOAD_SIZE_LIMIT: process.env.FILE_UPLOAD_SIZE_LIMIT ? Number(process.env.FILE_UPLOAD_SIZE_LIMIT) : 50 * 1024 * 1024, // 50M + // TODO: change this to localhost + SUBMISSIONS_API_URL: process.env.SUBMISSIONS_API_URL || 'https://api.topcoder-dev.com/v5/submissions', + MEMBERS_API_URL: process.env.MEMBERS_API_URL || 'https://api.topcoder-dev.com/v5/members', RESOURCES_API_URL: process.env.RESOURCES_API_URL || 'http://localhost:4000/v5/resources', // TODO: change this to localhost RESOURCE_ROLES_API_URL: process.env.RESOURCE_ROLES_API_URL || 'http://api.topcoder-dev.com/v5/resource-roles', diff --git a/src/common/helper.js b/src/common/helper.js index ee1f7507..f0cee0cc 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -959,6 +959,50 @@ async function getGroupById (groupId) { } } +/** + * Get challenge submissions + * @param {String} challengeId the challenge id + * @returns {Array} the submission + */ +async function getChallengeSubmissions (challengeId) { + const token = await getM2MToken() + let allSubmissions = [] + // get search is paginated, we need to get all pages' data + let page = 1 + while (true) { + const result = await axios.get(`${config.SUBMISSIONS_API_URL}?challengeId=${challengeId}`, { + headers: { Authorization: `Bearer ${token}` }, + params: { + page, + perPage: 100 + } + }) + const ids = result.data || [] + if (ids.length === 0) { + break + } + allSubmissions = allSubmissions.concat(ids) + page += 1 + if (result.headers['x-total-pages'] && page > Number(result.headers['x-total-pages'])) { + break + } + } + return allSubmissions +} + +/** + * Get member by ID + * @param {String} userId the user ID + * @returns {Object} + */ +async function getMemberById (userId) { + const token = await getM2MToken() + const res = await axios.get(`${config.MEMBERS_API_URL}?userId=${userId}`, { + headers: { Authorization: `Bearer ${token}` } + }) + return res.data || {} +} + module.exports = { wrapExpress, autoWrapExpress, @@ -1000,5 +1044,7 @@ module.exports = { ensureUserCanModifyChallenge, userHasFullAccess, sumOfPrizes, - getGroupById + getGroupById, + getChallengeSubmissions, + getMemberById } diff --git a/src/controllers/ChallengeController.js b/src/controllers/ChallengeController.js index 1e4cfbb5..56ef2b78 100644 --- a/src/controllers/ChallengeController.js +++ b/src/controllers/ChallengeController.js @@ -38,6 +38,16 @@ async function getChallenge (req, res) { res.send(result) } +/** + * Get challenge statistics + * @param {Object} req the request + * @param {Object} res the response + */ +async function getChallengeStatistics (req, res) { + const result = await service.getChallengeStatistics(req.authUser, req.params.challengeId) + res.send(result) +} + /** * Fully update challenge * @param {Object} req the request @@ -77,5 +87,6 @@ module.exports = { getChallenge, fullyUpdateChallenge, partiallyUpdateChallenge, - deleteChallenge + deleteChallenge, + getChallengeStatistics } diff --git a/src/routes.js b/src/routes.js index 05a54e00..7df4478e 100644 --- a/src/routes.js +++ b/src/routes.js @@ -61,6 +61,12 @@ module.exports = { scopes: [DELETE, ALL] } }, + '/challenges/:challengeId/statistics': { + get: { + controller: 'ChallengeController', + method: 'getChallengeStatistics', + } + }, '/challenge-types': { get: { controller: 'ChallengeTypeController', diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index b6278f4c..a72af8be 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1209,6 +1209,49 @@ getChallenge.schema = { id: Joi.id() } +/** + * Get challenge statistics + * @param {Object} currentUser the user who perform operation + * @param {String} id the challenge id + * @returns {Object} the challenge with given id + */ +async function getChallengeStatistics (currentUser, id) { + const challenge = await getChallenge(currentUser, id) + // for now, only Data Science challenges are supported + if (challenge.type !== 'Challenge' && challenge.track !== 'Data Science') { + throw new errors.BadRequestError(`Challenge of type ${challenge.type} and track ${challenge.track} does not support statistics`) + } + // get submissions + const submissions = await helper.getChallengeSubmissions(challenge.id) + // for each submission, load member profile + const map = {} + for (const submission of submissions) { + if (!map[submission.memberId]) { + // Load member profile and cache + const member = await helper.getMemberById(submission.memberId) + map[submission.memberId] = { + photoUrl: member.photoURL, + rating: _.get(member, 'maxRating.rating', 0), + ratingColor: _.get(member, 'maxRating.rating', '#9D9FA0'), + homeCountryCode: member.homeCountryCode, + handle: member.handle, + submissions: [] + } + } + // add submission + map[submission.memberId].submissions.push({ + created: submission.created, + score: _.get(_.find(submission.review || [], r => r.metadata), 'score', 0) + }) + } + return _.map(_.keys(map), (userId) => map[userId]) +} + +getChallengeStatistics.schema = { + currentUser: Joi.any(), + id: Joi.id() +} + /** * Check whether given two phases array are different. * @param {Array} phases the first phases array @@ -2078,7 +2121,8 @@ module.exports = { getChallenge, fullyUpdateChallenge, partiallyUpdateChallenge, - deleteChallenge + deleteChallenge, + getChallengeStatistics } logger.buildService(module.exports)