Skip to content
2 changes: 2 additions & 0 deletions __tests__/__snapshots__/index.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,10 @@ Object {
},
},
"submission": Object {
"default": undefined,
"getFinalScore": [Function],
"getProvisionalScore": [Function],
"processMMSubmissions": [Function],
},
"tc": Object {
"COMPETITION_TRACKS": Object {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"lint:js": "./node_modules/.bin/eslint --ext .js,.jsx .",
"test": "npm run lint && npm run jest"
},
"version": "0.8.2",
"version": "0.8.3",
"dependencies": {
"auth0-js": "^6.8.4",
"config": "^3.2.0",
Expand Down
78 changes: 67 additions & 11 deletions src/actions/challenge.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,48 @@
* @desc Actions related to Topcoder challenges APIs.
*/

/* global CONFIG */
import _ from 'lodash';
import { config } from 'topcoder-react-utils';
import { createActions } from 'redux-actions';
import { getService as getChallengesService } from '../services/challenges';
import { getService as getSubmissionService } from '../services/submissions';
import { getService as getMemberService } from '../services/members';
import { getApi } from '../services/api';
import * as submissionUtil from '../utils/submission';

const { PAGE_SIZE } = CONFIG;

/**
* Private. Loads from the backend all data matching some conditions.
* @param {Function} getter Given params object of shape { limit, offset }
* loads from the backend at most "limit" data, skipping the first
* "offset" ones. Returns loaded data as an array.
* @param {Number} page Optional. Next page of data to load.
* @param {Number} perPage Optional. The size of the page content to load.
* @param {Array} prev Optional. data loaded so far.
*/
function getAll(getter, page = 1, perPage = PAGE_SIZE, prev) {
/* Amount of submissions to fetch in one API call. 50 is the current maximum
* amount of submissions the backend returns, event when the larger limit is
* explicitely required. */
return getter({
page,
perPage,
}).then((res) => {
if (res.length === 0) {
return prev || res;
}
// parse submissions
let current = [];
if (prev) {
current = prev.concat(res);
} else {
current = res;
}
return getAll(getter, 1 + page, perPage, current);
});
}

/**
* @static
Expand Down Expand Up @@ -106,10 +142,26 @@ function getMMSubmissionsInit(challengeId) {
* @param {String} tokenV3 Topcoder auth token v3.
* @return {Action}
*/
async function getMMSubmissionsDone(challengeId, registrants, tokenV3) {
function getMMSubmissionsDone(challengeId, registrants, tokenV3) {
const filter = { challengeId };
const memberService = getMemberService(tokenV3);
const submissionsService = getSubmissionService(tokenV3);
const submissions = await submissionsService.getSubmissions(challengeId);
return { challengeId, submissions, tokenV3 };

// TODO: Move those numbers to configs
return getAll(params => submissionsService.getSubmissions(filter, params), 1, 500)
.then((submissions) => {
const userIds = _.uniq(_.map(submissions, sub => sub.memberId));
return memberService.getMembersInformation(userIds)
.then((resources) => {
const finalSubmissions = submissionUtil
.processMMSubmissions(submissions, resources, registrants);
return {
challengeId,
submissions: finalSubmissions,
tokenV3,
};
});
});
}

/**
Expand Down Expand Up @@ -319,8 +371,8 @@ function getActiveChallengesCountDone(handle, tokenV3) {
* @param {String} submissionId The submission id
* @return {Action}
*/
function getSubmissionInformationInit(submissionId) {
return _.toString(submissionId);
function getSubmissionInformationInit(challengeId, submissionId) {
return { challengeId: _.toString(challengeId), submissionId: _.toString(submissionId) };
}

/**
Expand All @@ -330,12 +382,16 @@ function getSubmissionInformationInit(submissionId) {
* @param {String} tokenV3 Topcoder auth token v3.
* @return {Action}
*/
function getSubmissionInformationDone(submissionId, tokenV3) {
return getSubmissionService(tokenV3)
.getSubmissionInformation(submissionId)
.then(response => ({
submissionId, submission: response,
}));
function getSubmissionInformationDone(challengeId, submissionId, tokenV3) {
const filter = { challengeId };
const submissionsService = getSubmissionService(tokenV3);

return getAll(params => submissionsService.getSubmissions(filter, params), 1, 500)
.then((submissions) => {
const submission = _.find(submissions, { id: submissionId });
_.remove(submission.review, review => review.typeId === CONFIG.AV_SCAN_SCORER_REVIEW_TYPE_ID);
return { submissionId, submission };
});
}

export default createActions({
Expand Down
3 changes: 2 additions & 1 deletion src/reducers/challenge.js
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,8 @@ function onGetActiveChallengesCountDone(state, { payload, error }) {
function onGetSubmissionInformationInit(state, action) {
return {
...state,
loadingSubmissionInformationForSubmissionId: action.payload,
loadingSubmissionInformationForChallengeId: action.payload.challengeId,
loadingSubmissionInformationForSubmissionId: action.payload.submissionId,
submissionInformation: null,
};
}
Expand Down
4 changes: 0 additions & 4 deletions src/services/groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,6 @@ export function checkUserGroups(groupIds, userGroups, knownGroups) {
function handleApiResponse(response) {
if (!response.ok) throw new Error(response.statusText);
return response.json();
// return response.json().then(({ result }) => {
// return result;
// if (result.status !== 200) throw new Error(result.content);
// });
}

/**
Expand Down
28 changes: 18 additions & 10 deletions src/services/submissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @desc This module provides a service for convenient manipulation with
* Topcoder submissions via TC API. Currently only used for MM challenges
*/

import qs from 'qs';
import { getApi } from './api';

/**
Expand All @@ -16,20 +16,27 @@ class SubmissionsService {
*/
constructor(tokenV3) {
this.private = {
broker: getApi('MM_BROKER', tokenV3),
apiV5: getApi('V5', tokenV3),
tokenV3,
};
}

/**
* Get submissions of challenge
* @param {Object} challengeId
* @param {Object} filters
* @param {Object} params
* @return {Promise} Resolves to the api response.
*/
async getSubmissions(challengeId) {
const url = `/v5/submissions?challengeId=${challengeId}`;
return this.private.broker.get(url)
.then(res => (res.ok ? res.json() : new Error(res.statusText)));
async getSubmissions(filters, params) {
const query = {
...filters,
...params,
};

const url = `/submissions?${qs.stringify(query, { encode: false })}`;
return this.private.apiV5.get(url)
.then(res => (res.ok ? res.json() : new Error(res.statusText)))
.then(res => res);
}

/**
Expand All @@ -38,9 +45,10 @@ class SubmissionsService {
* @returns {Promise} Resolves to the api response.
*/
async getSubmissionInformation(submissionId) {
const url = `/v5/submissions/${submissionId}`;
return this.private.broker.get(url)
.then(res => (res.ok ? res.json() : new Error(res.statusText)));
const url = `/submissions/${submissionId}`;
return this.private.apiV5.get(url)
.then(res => (res.ok ? res.json() : new Error(res.statusText)))
.then(res => res);
}
}

Expand Down
141 changes: 141 additions & 0 deletions src/utils/submission.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,84 @@
/**
* Various submissions functions.
*/
/* global CONFIG */
/* eslint-disable no-param-reassign */
import _ from 'lodash';

const { AV_SCAN_SCORER_REVIEW_TYPE_ID } = CONFIG;

function removeDecimal(num) {
const re = new RegExp('^-?\\d+');
return num.toString().match(re)[0];
}

function toAcurateFixed(num, decimal) {
const re = new RegExp(`^-?\\d+(?:.\\d{0,${(decimal)}})?`);
return num.toString().match(re)[0];
}

function toFixed(num, decimal) {
if (_.isNaN(parseFloat(num))) return num;
num = parseFloat(num);

const result = _.toFinite(toAcurateFixed(num, decimal));
const integerResult = _.toFinite(removeDecimal(num));

if (_.isInteger(result)) {
return integerResult;
}
return result;
}

function getMMChallengeHandleStyle(handle, registrants) {
const style = _.get(_.find(registrants, m => m.handle === handle), 'colorStyle', null);
if (style) return JSON.parse(style.replace(/(\w+):\s*([^;]*)/g, '{"$1": "$2"}'));
return {};
}

/**
* Process each submission rank of MM challenge
* @param submissions the array of submissions
*/
function processRanks(submissions) {
let maxFinalScore = 0;
submissions.sort((a, b) => {
let pA = _.get(a, 'submissions[0]', { provisionalScore: 0 }).provisionalScore;
let pB = _.get(b, 'submissions[0]', { provisionalScore: 0 }).provisionalScore;
if (pA === '-') pA = 0;
if (pB === '-') pB = 0;
if (pA === pB) {
const timeA = new Date(_.get(a, 'submissions[0].submissionTime'));
const timeB = new Date(_.get(b, 'submissions[0].submissionTime'));
return timeA - timeB;
}
return pB - pA;
});
_.each(submissions, (submission, i) => {
submissions[i].provisionalRank = i + 1;
});

submissions.sort((a, b) => {
let pA = _.get(a, 'submissions[0]', { finalScore: 0 }).finalScore;
let pB = _.get(b, 'submissions[0]', { finalScore: 0 }).finalScore;
if (pA === '-') pA = 0;
if (pB === '-') pB = 0;
if (pA > 0) maxFinalScore = pA;
if (pB > 0) maxFinalScore = pB;
if (pA === pB) {
const timeA = new Date(_.get(a, 'submissions[0].submissionTime'));
const timeB = new Date(_.get(b, 'submissions[0].submissionTime'));
return timeA - timeB;
}
return pB - pA;
});
if (maxFinalScore > 0) {
_.each(submissions, (submission, i) => {
submissions[i].finalRank = i + 1;
});
}
return { submissions, maxFinalScore };
}

/**
* Get provisional score of submission
Expand Down Expand Up @@ -33,3 +111,66 @@ export function getFinalScore(submission) {
}
return finalScore;
}

/**
* Process submissions of MM challenge
* @param submissions the array of submissions
* @param resources the challenge resources
* @param registrants the challenge registrants
*/
export function processMMSubmissions(submissions, resources, registrants) {
const data = {};
const result = [];

_.each(submissions, (submission) => {
const { memberId } = submission;
let memberHandle;
const resource = _.find(resources, r => _.get(r, 'userId').toString() === memberId.toString());
if (_.isEmpty(resource)) {
memberHandle = memberId;
} else {
memberHandle = _.has(resource, 'handle') ? _.get(resource, 'handle') : memberId.toString();
}
if (!data[memberHandle]) {
data[memberHandle] = [];
}
const validReviews = _.filter(submission.review,
r => !_.isEmpty(r) && (r.typeId !== AV_SCAN_SCORER_REVIEW_TYPE_ID));
validReviews.sort((a, b) => {
const dateA = new Date(a.created);
const dateB = new Date(b.created);
return dateB - dateA;
});

const provisionalScore = toFixed(_.get(validReviews, '[0].score', '-'), 5);
const finalScore = toFixed(_.get(submission, 'reviewSummation[0].aggregateScore', '-'), 5);

data[memberHandle].push({
submissionId: submission.id,
submissionTime: submission.created,
provisionalScore,
finalScore,
});
});

_.each(data, (value, key) => {
result.push({
submissions: [...value.sort((a, b) => new Date(b.submissionTime)
.getTime() - new Date(a.submissionTime).getTime())],
member: key,
colorStyle: getMMChallengeHandleStyle(key, registrants),
});
});

const { submissions: finalSubmissions, maxFinalScore } = processRanks(result);
finalSubmissions.sort((a, b) => {
if (maxFinalScore === 0) {
return a.provisionalRank - b.provisionalRank;
}
return a.finalRank - b.finalRank;
});

return finalSubmissions;
}

export default undefined;