From f88acb3f254d47ddd6111abe8f71d4f623fa9039 Mon Sep 17 00:00:00 2001
From: Huan Li Action
* [.getActiveChallengesCountInit()](#module_actions.challenge.getActiveChallengesCountInit) ⇒ Action
* [.getActiveChallengesCountDone(handle, tokenV3)](#module_actions.challenge.getActiveChallengesCountDone) ⇒ Action
+ * [.getMMSubmissionsInit(challengeId)](#module_actions.challenge.getMMSubmissionsInit) ⇒ Action
+ * [.getMMSubmissionsDone(challengeId, submitterIds, registrants, tokenV3)](#module_actions.challenge.getMMSubmissionsDone) ⇒ Action
@@ -31,14 +33,14 @@ Actions related to Topcoder challenges APIs.
Creates an action that drops from Redux store all checkpoints loaded
before.
-**Kind**: static method of [actions.challenge
](#module_actions.challenge)
+**Kind**: static method of [actions.challenge
](#module_actions.challenge)
### actions.challenge.dropResults() ⇒ Action
Creates an action that drops from Redux store all challenge results
loaded before.
-**Kind**: static method of [actions.challenge
](#module_actions.challenge)
+**Kind**: static method of [actions.challenge
](#module_actions.challenge)
### actions.challenge.getDetailsInit(challengeId) ⇒ Action
@@ -93,7 +95,7 @@ challenge.
Creates an action that signals beginning of registration for a
challenge.
-**Kind**: static method of [actions.challenge
](#module_actions.challenge)
+**Kind**: static method of [actions.challenge
](#module_actions.challenge)
### actions.challenge.registerDone(auth, challengeId) ⇒ Action
@@ -114,7 +116,7 @@ Creates an action that registers user for a challenge.
Creates an action that signals beginning of user unregistration from a
challenge.
-**Kind**: static method of [actions.challenge
](#module_actions.challenge)
+**Kind**: static method of [actions.challenge
](#module_actions.challenge)
### actions.challenge.unregisterDone(auth, challengeId) ⇒ Action
@@ -161,7 +163,7 @@ Creates an action that loads challenge results.
Creates an action that signals beginning of challenge checkpoints data
loading.
-**Kind**: static method of [actions.challenge
](#module_actions.challenge)
+**Kind**: static method of [actions.challenge
](#module_actions.challenge)
### actions.challenge.fetchCheckpointsDone(tokenV2, challengeId)
@@ -180,7 +182,7 @@ Creates an action that loads challenge checkpoints data.
Creates an action that Toggles checkpoint details panel in the Topcoder
Submission Management Page.
-**Kind**: static method of [actions.challenge
](#module_actions.challenge)
+**Kind**: static method of [actions.challenge
](#module_actions.challenge)
**Todo**
- [ ] This is UI action relevant to a specific page in specific app. Must be
@@ -197,7 +199,7 @@ Creates an action that Toggles checkpoint details panel in the Topcoder
### actions.challenge.updateChallengeInit(uuid) ⇒ Action
Creates an action that signals beginning of challenge details update.
-**Kind**: static method of [actions.challenge
](#module_actions.challenge)
+**Kind**: static method of [actions.challenge
](#module_actions.challenge)
**Todo**
- [ ] No idea, why we have this action. This functionality should be covered
@@ -214,7 +216,7 @@ Creates an action that signals beginning of challenge details update.
### actions.challenge.updateChallengeDone(uuid, challenge, tokenV3) ⇒ Action
Creates an action that updates challenge details.
-**Kind**: static method of [actions.challenge
](#module_actions.challenge)
+**Kind**: static method of [actions.challenge
](#module_actions.challenge)
**Todo**
- [ ] No idea, why we have this action. This functionality should be covered
@@ -233,7 +235,7 @@ Creates an action that updates challenge details.
### actions.challenge.getActiveChallengesCountInit() ⇒ Action
Creates an action that signals beginning of getting count of user's active challenges.
-**Kind**: static method of [actions.challenge
](#module_actions.challenge)
+**Kind**: static method of [actions.challenge
](#module_actions.challenge)
### actions.challenge.getActiveChallengesCountDone(handle, tokenV3) ⇒ Action
@@ -246,3 +248,28 @@ Creates an action that gets count of user's active challenges from the backend.
| handle | String
| Topcoder user handle. |
| tokenV3 | String
| Optional. Topcoder auth token v3. Without token only public challenges will be counted. With the token provided, the action will also count private challenges related to this user. |
+
+
+### actions.challenge.getMMSubmissionsInit(challengeId) ⇒ Action
+Creates an action that signals beginning of Marathon Match submissions loading.
+
+**Kind**: static method of [actions.challenge
](#module_actions.challenge)
+
+
+| Param | Type | Description |
+| --- | --- | --- |
+| challengeId | String
| The challenge id. |
+
+### actions.challenge.getMMSubmissionsDone(challengeId, submitterIds, registrants, tokenV3) ⇒ Action
+Creates an action that loads Marathon Match submissions to the specified
+challenge.
+
+**Kind**: static method of [actions.challenge
](#module_actions.challenge)
+
+
+| Param | Type | Description |
+| --- | --- | --- |
+| challengeId | String
| The challenge id. |
+| submitterIds | Array
| The ids of submitters |
+| registrants | Array
| The registrants of challenge |
+| tokenV3 | String
| Topcoder auth token v3. |
diff --git a/docs/index.md b/docs/index.md
index 185c46d5..9139f14a 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -143,6 +143,12 @@ action to really cancel group loading.
Reducer for state.terms.
his module provides a service for convenient manipulation with + Topcoder submissions via TC API.
+This module provides a service for conventient access to Topcoder APIs.
Collection of url function.
Collection of submission function.
+Promise
* _inner_
+ * [~onGetMMSubmissionsInit(state, action)](#module_reducers.challenge..onGetMMSubmissionsInit) ⇒ Object
+ * [~onGetMMSubmissionsDone(state, action)](#module_reducers.challenge..onGetDetailsDone) ⇒ Object
* [~onGetDetailsInit(state, action)](#module_reducers.challenge..onGetDetailsInit) ⇒ Object
* [~onGetDetailsDone(state, action)](#module_reducers.challenge..onGetDetailsDone) ⇒ Object
* [~onGetSubmissionsInit(state, action)](#module_reducers.challenge..onGetSubmissionsInit) ⇒ Object
@@ -34,7 +36,7 @@ State segment managed by this reducer has the following strcuture:
### reducers.challenge.default
Reducer with default intial state.
-**Kind**: static property of [reducers.challenge
](#module_reducers.challenge)
+**Kind**: static property of [reducers.challenge
](#module_reducers.challenge)
### reducers.challenge.factory(options) ⇒ Promise
@@ -42,7 +44,7 @@ Factory which creates a new reducer with its initial state tailored to the
given options object, if specified (for server-side rendering). If options
object is not specified, it creates just the default reducer. Accepted options are:
-**Kind**: static method of [reducers.challenge
](#module_reducers.challenge)
+**Kind**: static method of [reducers.challenge
](#module_reducers.challenge)
**Resolves**: Function(state, action): state
New reducer.
| Param | Type | Default | Description |
@@ -53,18 +55,48 @@ object is not specified, it creates just the default reducer. Accepted options a
| [options.challenge.challengeDetails.id] | String
| ''
| Optional. ID of the challenge to load details for. |
| [options.challenge.challengeDetails.mySubmission] | Boolean
| false
| Optional. The flag indicates whether load my submission. |
+
+
+### reducers.challenge~onGetMMSubmissionsInit(state, action) ⇒ Object
+Handles CHALLENGE/GET_MM_SUBMISSION_INIT action.
+
+**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
+**Returns**: Object
- New state
+
+| Param | Type |
+| --- | --- |
+| state | Object
|
+| action | Object
|
+
+
+
+### reducers.challenge~onGetMMSubmissionsDone(state, action) ⇒ Object
+Handles CHALLENGE/GET_MM_SUBMISSION_DONE action.
+Note, that it silently discards received details if the ID of received
+challenge mismatches the one stored in loadingMMSubmissionsForChallengeId field
+of the state.
+
+**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
+**Returns**: Object
- New state.
+
+| Param | Type |
+| --- | --- |
+| state | Object
|
+| action | Object
|
+
+
### reducers.challenge~onGetDetailsInit(state, action) ⇒ Object
Handles CHALLENGE/GET_DETAILS_INIT action.
-**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
+**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
**Returns**: Object
- New state
| Param | Type |
| --- | --- |
-| state | Object
|
-| action | Object
|
+| state | Object
|
+| action | Object
|
@@ -74,26 +106,26 @@ Note, that it silently discards received details if the ID of received
challenge mismatches the one stored in loadingDetailsForChallengeId field
of the state.
-**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
+**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
**Returns**: Object
- New state.
| Param | Type |
| --- | --- |
-| state | Object
|
-| action | Object
|
+| state | Object
|
+| action | Object
|
### reducers.challenge~onGetSubmissionsInit(state, action) ⇒ Object
Handles CHALLENGE/GET_SUBMISSION_INIT action.
-**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
+**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
**Returns**: Object
- New state.
| Param | Type |
| --- | --- |
-| state | Object
|
-| action | Object
|
+| state | Object
|
+| action | Object
|
@@ -128,8 +160,8 @@ Handles CHALLENGE/LOAD_RESULTS_INIT action.
| Param | Type |
| --- | --- |
-| state | Object
|
-| action | Object
|
+| state | Object
|
+| action | Object
|
@@ -141,7 +173,7 @@ Handles CHALLENGE/LOAD_RESULTS_DONE action.
| Param | Type |
| --- | --- |
| state | Object
|
-| action | Object
|
+| action | Object
|
@@ -164,15 +196,15 @@ Handles CHALLENGE/UNREGISTER_DONE action.
| Param | Type |
| --- | --- |
-| state | Object
|
-| action | Object
|
+| state | Object
|
+| action | Object
|
### reducers.challenge~onUpdateChallengeInit(state, actions) ⇒ Object
Handles CHALLENGE/UPDATE_CHALLENGE_INIT.
-**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
+**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
**Returns**: Object
- New state.
| Param | Type | Description |
@@ -185,7 +217,7 @@ Handles CHALLENGE/UPDATE_CHALLENGE_INIT.
### reducers.challenge~onUpdateChallengeDone(state, actions) ⇒ Object
Handles CHALLENGE/UPDATE_CHALLENGE_DONE.
-**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
+**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
**Returns**: Object
- New state.
| Param | Type | Description |
@@ -198,7 +230,7 @@ Handles CHALLENGE/UPDATE_CHALLENGE_DONE.
### reducers.challenge~onGetActiveChallengesCountDone(state, action) ⇒ Object
Handles CHALLENGE/GET_ACTIVE_CHALLENGES_COUNT_DONE action.
-**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
+**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
**Returns**: Object
- New state
| Param | Type | Description |
@@ -211,7 +243,7 @@ Handles CHALLENGE/GET_ACTIVE_CHALLENGES_COUNT_DONE action.
### reducers.challenge~create(initialState) ⇒ function
Creates a new Challenge reducer with the specified initial state.
-**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
+**Kind**: inner method of [reducers.challenge
](#module_reducers.challenge)
**Returns**: function
- Challenge reducer.
| Param | Type | Description |
diff --git a/docs/services.submissions.md b/docs/services.submissions.md
new file mode 100644
index 00000000..201297e5
--- /dev/null
+++ b/docs/services.submissions.md
@@ -0,0 +1,57 @@
+
+
+## services.submissions
+This module provides a service for searching for Topcoder
+submissions via API V5.
+
+* [services.submissions](#module_services.submissions)
+ * _static_
+ * [.getService(tokenV3)](#module_services.submissions.getService) ⇒ SubmissionService
+ * _inner_
+ * [~SubmissionsService](#module_services.submissions..SubmissionsService)
+ * [new SubmissionsService(tokenV3)](#new_module_services.submissions..SubmissionsService_new)
+ * [.getSubmissions(filters, params)](#module_services.submissions..SubmissionsService+getSubmissions) ⇒ Promise
+
+
+
+### services.submissions.getService(tokenV3) ⇒ SubmissionsService
+Returns a new or existing submissions service.
+
+**Kind**: static method of [services.submissions
](#module_services.submissions)
+**Returns**: SubmissionsService
- Submissions service object
+
+| Param | Type | Description |
+| --- | --- | --- |
+| tokenV3 | String
| Auth token for Topcoder API v5. |
+
+
+
+### services.submissions~SubmissionsService
+Service class.
+
+**Kind**: inner class of [services.submissions
](#module_services.submissions)
+
+* [~SubmissionsService](#module_services.submissions..SubmissionsService)
+ * [new SubmissionsService(tokenV3)](#new_module_services.submissions..SubmissionsService_new)
+ * [.getSubmissions(filters, params)](#module_services.submissions..SubmissionsService+getSubmissions) ⇒ Promise
+
+
+
+#### new SubmissionsService(tokenV3)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| tokenV3 | String
| Auth token for Topcoder API v5. |
+
+
+
+#### submissionsService.getSubmissions(filters, params) ⇒ Promise
+Get submissions of challenge
+
+**Kind**: instance method of [SubmissionsService
](#module_services.submissions..SubmissionsService)
+**Returns**: Promise
- Resolves to the submissions array.
+
+| Param | Type | Description |
+| --- | --- | --- |
+| filters | Object
| The filters object |
+| params | Object
| The params object |
diff --git a/docs/submission.md b/docs/submission.md
new file mode 100644
index 00000000..ff48298b
--- /dev/null
+++ b/docs/submission.md
@@ -0,0 +1,38 @@
+
+
+## submission
+Collection of submission functions.
+
+* [submission](#module_submission)
+ * [.getProvisionalScore(submission)](#module_submission.getProvisionalScore)
+ * [.getFinalScore(submission)](#module_submission.getFinalScore)
+ * [.processMMSubmissions(submissions, resources, registrants)](#module_submission.processMMSubmissions)
+
+
+### submission.getProvisionalScore(submission)
+Get provisional score of submission
+**Kind**: static method of [submission
](#module_submission)
+
+| Param | Type |
+| --- | --- |
+| submission | Object
|
+
+
+### submission.getFinalScore(submission)
+Get final score of submission
+**Kind**: static method of [submission
](#module_submission)
+
+| Param | Type |
+| --- | --- |
+| submission | Object
|
+
+
+### submission.processMMSubmissions(submissions, resources, registrants)
+Process submissions of MM challenge
+**Kind**: static method of [submission
](#module_submission)
+
+| Param | Type |
+| --- | --- |
+| submission | Object
|
+| resources | Array
|
+| registrants | Array
|
diff --git a/package.json b/package.json
index a8163f0b..30247526 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
"version": "0.7.15",
"dependencies": {
"auth0-js": "^6.8.4",
+ "config": "^3.1.0",
"isomorphic-fetch": "^2.2.1",
"le_node": "^1.7.0",
"lodash": "^4.17.10",
diff --git a/src/actions/challenge-listing.js b/src/actions/challenge-listing.js
index e60f10f0..1ab0d8f5 100644
--- a/src/actions/challenge-listing.js
+++ b/src/actions/challenge-listing.js
@@ -1,7 +1,7 @@
/**
* Challenge listing actions.
*/
-
+/* global CONFIG */
import _ from 'lodash';
import { createActions } from 'redux-actions';
import { decodeToken } from 'tc-accounts';
@@ -10,12 +10,12 @@ import { processSRM, COMPETITION_TRACKS } from '../utils/tc';
import { services } from '../services';
import { errors } from '../utils';
import * as filterUtil from '../utils/challenge/filter';
-import * as config from '../config';
+
const { fireErrorMessage } = errors;
const { getService } = services.challenge;
const { getReviewOpportunitiesService } = services.reviewOpportunities;
-const { PAGE_SIZE, REVIEW_OPPORTUNITY_PAGE_SIZE } = config;
+const { PAGE_SIZE, REVIEW_OPPORTUNITY_PAGE_SIZE } = CONFIG;
/**
* Process filter
diff --git a/src/actions/challenge.js b/src/actions/challenge.js
index f5dfb09b..9c00f33f 100644
--- a/src/actions/challenge.js
+++ b/src/actions/challenge.js
@@ -2,12 +2,48 @@
* @module "actions.challenge"
* @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 {Array} prev Optional. data loaded so far.
+ */
+function getAll(getter, page = 1, 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: PAGE_SIZE,
+ }).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, current);
+ });
+}
+
/**
* @static
@@ -83,6 +119,49 @@ function getSubmissionsDone(challengeId, tokenV2) {
});
}
+/**
+ * @static
+ * @desc Creates an action that signals beginning of Marathon Match submissions loading.
+ * @param {String} challengeId Challenge ID.
+ * @return {Action}
+ */
+function getMMSubmissionsInit(challengeId) {
+ /* As a safeguard, we enforce challengeId to be string (in case somebody
+ * passes in a number, by mistake). */
+ return _.toString(challengeId);
+}
+
+
+/**
+ * @static
+ * @desc Creates an action that loads Marathon Match submissions to the specified
+ * challenge.
+ * @param {String} challengeId Challenge ID.
+ * @param {Array} submitterIds The array of submitter ids.
+ * @param {Array} registrants The array of register.
+ * @param {String} tokenV3 Topcoder auth token v3.
+ * @return {Action}
+ */
+function getMMSubmissionsDone(challengeId, submitterIds, registrants, tokenV3) {
+ const filter = { challengeId };
+ const memberService = getMemberService(tokenV3);
+ const submissionsService = getSubmissionService(tokenV3);
+ const calls = [
+ memberService.getMembersInformation(submitterIds),
+ getAll(params => submissionsService.getSubmissions(filter, params)),
+ ];
+ return Promise.all(calls).then(([resources, submissions]) => {
+ const finalSubmissions = submissionUtil
+ .processMMSubmissions(submissions, resources, registrants);
+ return {
+ challengeId,
+ submissions: finalSubmissions,
+ tokenV3,
+ };
+ });
+}
+
+
/**
* @static
* @desc Creates an action that signals beginning of registration for a
@@ -305,5 +384,7 @@ export default createActions({
UPDATE_CHALLENGE_DONE: updateChallengeDone,
GET_ACTIVE_CHALLENGES_COUNT_INIT: getActiveChallengesCountInit,
GET_ACTIVE_CHALLENGES_COUNT_DONE: getActiveChallengesCountDone,
+ GET_MM_SUBMISSIONS_INIT: getMMSubmissionsInit,
+ GET_MM_SUBMISSIONS_DONE: getMMSubmissionsDone,
},
});
diff --git a/src/config/index.js b/src/config/index.js
deleted file mode 100644
index 2a5ab790..00000000
--- a/src/config/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = {
- PAGE_SIZE: 50,
- REVIEW_OPPORTUNITY_PAGE_SIZE: 1000,
-};
diff --git a/src/index.js b/src/index.js
index 15b0e7d7..7dcdfde6 100644
--- a/src/index.js
+++ b/src/index.js
@@ -12,5 +12,5 @@ export { actions } from './actions';
export { services } from './services';
export {
- challenge, logger, errors, tc, time, mock, url,
+ challenge, logger, errors, tc, time, mock, url, submission,
} from './utils';
diff --git a/src/reducers/challenge.js b/src/reducers/challenge.js
index 889623e1..2d5b47c9 100644
--- a/src/reducers/challenge.js
+++ b/src/reducers/challenge.js
@@ -113,6 +113,44 @@ function onGetSubmissionsDone(state, action) {
};
}
+/**
+ * Handles CHALLENGE/GET_MM_SUBMISSION_INIT action.
+ * @param {Object} state
+ * @param {Object} action
+ * @return {Object} New state.
+ */
+function onGetMMSubmissionsInit(state, action) {
+ return {
+ ...state,
+ loadingMMSubmissionsForChallengeId: action.payload,
+ mmSubmissions: [],
+ };
+}
+
+/**
+ * Handles CHALLENGE/GET_MM_SUBMISSION_DONE action.
+ * @param {Object} state Previous state.
+ * @param {Object} action Action.
+ */
+function onGetMMSubmissionsDone(state, action) {
+ if (action.error) {
+ logger.error('Failed to get Marathon Match submissions for the challenge', action.payload);
+ return {
+ ...state,
+ loadingMMSubmissionsForChallengeId: '',
+ mmSubmissions: [],
+ };
+ }
+
+ const { challengeId, submissions } = action.payload;
+ if (challengeId.toString() !== state.loadingMMSubmissionsForChallengeId) return state;
+ return {
+ ...state,
+ loadingMMSubmissionsForChallengeId: '',
+ mmSubmissions: submissions,
+ };
+}
+
/**
* Handles challengeActions.fetchCheckpointsDone action.
* @param {Object} state Previous state.
@@ -294,6 +332,8 @@ function create(initialState) {
[a.getDetailsDone]: onGetDetailsDone,
[a.getSubmissionsInit]: onGetSubmissionsInit,
[a.getSubmissionsDone]: onGetSubmissionsDone,
+ [a.getMmSubmissionsInit]: onGetMMSubmissionsInit,
+ [a.getMmSubmissionsDone]: onGetMMSubmissionsDone,
[smpActions.smp.deleteSubmissionDone]: (state, { payload }) => ({
...state,
mySubmissions: {
@@ -324,6 +364,7 @@ function create(initialState) {
loadingCheckpoints: false,
loadingDetailsForChallengeId: '',
loadingResultsForChallengeId: '',
+ loadingMMSubmissionsForChallengeId: '',
mySubmissions: {},
checkpoints: null,
registering: false,
@@ -331,6 +372,7 @@ function create(initialState) {
resultsLoadedForChallengeId: '',
unregistering: false,
updatingChallengeUuid: '',
+ mmSubmissions: [],
}));
}
diff --git a/src/services/api.js b/src/services/api.js
index 7c1e3355..97a045a1 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -264,6 +264,7 @@ export function getApi(version, token) {
export const getApiV2 = token => getApi('V2', token);
export const getApiV3 = token => getApi('V3', token);
export const getApiV4 = token => getApi('V4', token);
+export const getApiV5 = token => getApi('V5', token);
/**
* Gets a valid TC M2M token, either requesting one from TC Auth0 API, or
diff --git a/src/services/index.js b/src/services/index.js
index b9707791..4d776832 100644
--- a/src/services/index.js
+++ b/src/services/index.js
@@ -14,6 +14,7 @@ import * as userSetting from './user-settings';
import * as user from './user';
import * as lookup from './lookup';
import * as userTraits from './user-traits';
+import * as submissions from './submissions';
export const services = {
api,
@@ -29,6 +30,7 @@ export const services = {
reviewOpportunities,
lookup,
userTraits,
+ submissions,
};
export default undefined;
diff --git a/src/services/members.js b/src/services/members.js
index 62afbf36..9afa0311 100644
--- a/src/services/members.js
+++ b/src/services/members.js
@@ -289,6 +289,18 @@ class MembersService {
const res = await this.private.api.get(`/members/${handle}/verify?token=${emailVerifyToken}`);
return getApiResponsePayload(res);
}
+
+ /**
+ * Get members information
+ * @param {Array} userIds the member ids
+ */
+ async getMembersInformation(userIds) {
+ const query = `query=${encodeURI(_.map(userIds, id => `userId:${id}`).join(' OR '))}`;
+ const limit = `limit=${userIds.length}`;
+ const url = `/members/_search?fields=userId%2Chandle%2CphotoURL%2CfirstName%2ClastName&${query}&${limit}`;
+ const res = await this.private.api.get(url);
+ return getApiResponsePayload(res);
+ }
}
let lastInstance = null;
diff --git a/src/services/submissions.js b/src/services/submissions.js
new file mode 100644
index 00000000..6de0e6c2
--- /dev/null
+++ b/src/services/submissions.js
@@ -0,0 +1,57 @@
+/**
+ * @module "services.submission"
+ * @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';
+
+/**
+ * Submission service.
+ */
+class SubmissionsService {
+ /**
+ * Creates a new SubmissionService instance.
+ * @param {String} tokenV3 Optional. Auth token for Topcoder API v3.
+ */
+ constructor(tokenV3) {
+ this.private = {
+ apiV5: getApi('V5', tokenV3),
+ tokenV3,
+ };
+ }
+
+ /**
+ * Get submissions of challenge
+ * @param {Object} filters
+ * @param {Object} params
+ * @return {Promise} Resolves to the api response.
+ */
+ 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);
+ }
+}
+
+let lastInstance = null;
+/**
+ * Returns a new or existing submissions service.
+ * @param {String} tokenV3 Optional. Auth token for Topcoder API v3.
+ * @return {SubmissionsService} Submissions service object
+ */
+export function getService(tokenV3) {
+ if (!lastInstance || lastInstance.private.tokenV3 !== tokenV3) {
+ lastInstance = new SubmissionsService(tokenV3);
+ }
+ return lastInstance;
+}
+
+/* Using default export would be confusing in this case. */
+export default undefined;
diff --git a/src/utils/index.js b/src/utils/index.js
index e7e4ff52..e3fc752f 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -10,6 +10,7 @@ import * as filter from './challenge/filter';
import * as buckets from './challenge/buckets';
import * as sort from './challenge/sort';
import * as url from './url';
+import * as submission from './submission';
const challenge = {
filter,
@@ -25,4 +26,5 @@ export {
mock,
errors,
url,
+ submission,
};
diff --git a/src/utils/submission.js b/src/utils/submission.js
new file mode 100644
index 00000000..d3bec7e4
--- /dev/null
+++ b/src/utils/submission.js
@@ -0,0 +1,177 @@
+/**
+ * Various submissions functions.
+ */
+/* global CONFIG */
+/* eslint-disable no-param-reassign */
+import _ from 'lodash';
+
+const { AV_SCAN_SCORER_REVIEW_TYPE_ID } = CONFIG;
+
+function round(num, decimal) {
+ if (_.isNaN(num)) {
+ return 0;
+ }
+ const p1 = 10 ** (decimal + 1);
+ const p2 = 10 ** decimal;
+ return Math.round(num * p1 / 10) / p2;
+}
+
+function removeDecimal(num, decimal) {
+ return ((num % decimal) + decimal) % decimal;
+}
+
+function toFixed(num, decimal) {
+ const result = _.toFinite(round(num, decimal).toFixed(decimal));
+ const integerResult = _.toFinite(removeDecimal(result, decimal));
+ if (_.isInteger(integerResult)) {
+ 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;
+ return pB - pA;
+ });
+ _.each(submissions, (submission, i) => {
+ submissions[i].provisionalRank = i + 1;
+ });
+
+ submissions.sort((a, b) => {
+ let pA = _.get(a, 'submissions[0]', { provisionalScore: 0 }).finalScore;
+ let pB = _.get(b, 'submissions[0]', { provisionalScore: 0 }).finalScore;
+ if (pA === '-') pA = 0;
+ if (pB === '-') pB = 0;
+ if (pA > 0) maxFinalScore = pA;
+ if (pB > 0) maxFinalScore = pB;
+ return pB - pA;
+ });
+ if (maxFinalScore > 0) {
+ _.each(submissions, (submission, i) => {
+ submissions[i].finalRank = i + 1;
+ });
+ }
+ return { submissions, maxFinalScore };
+}
+
+/**
+ * Get provisional score of submission
+ * @param submission
+ */
+export function getProvisionalScore(submission) {
+ const { submissions: subs } = submission;
+ if (!subs || subs.length === 0) {
+ return 0;
+ }
+ const { provisionalScore } = subs[0];
+ if (!provisionalScore || provisionalScore < 0) {
+ return 0;
+ }
+ return provisionalScore;
+}
+
+/**
+ * Get final score of submission
+ * @param submission
+ */
+export function getFinalScore(submission) {
+ const { submissions: subs } = submission;
+ if (!subs || subs.length === 0) {
+ return 0;
+ }
+ const { finalScore } = subs[0];
+ if (!finalScore || finalScore < 0) {
+ return 0;
+ }
+ 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;
+ });
+ let provisionalScore;
+ if (validReviews.length > 0) {
+ provisionalScore = _.get(validReviews, '[0].score', 0);
+ if (_.isString(provisionalScore)) provisionalScore = _.toFinite(provisionalScore);
+ provisionalScore = toFixed(provisionalScore, 5);
+ } else {
+ provisionalScore = -1;
+ }
+
+ let finalScore = _.get(submission, 'reviewSummation[0].aggregateScore', 0);
+ if (_.isString(finalScore)) finalScore = _.toFinite(finalScore);
+ if (finalScore > 0) {
+ finalScore = toFixed(finalScore, 5);
+ } else {
+ finalScore = 0;
+ }
+ 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;