diff --git a/__tests__/__snapshots__/index.js.snap b/__tests__/__snapshots__/index.js.snap
index 7bc989b4..53d2317a 100644
--- a/__tests__/__snapshots__/index.js.snap
+++ b/__tests__/__snapshots__/index.js.snap
@@ -111,6 +111,8 @@ Object {
"getSubtrackChallengesInit": [Function],
"getUserMarathonDone": [Function],
"getUserMarathonInit": [Function],
+ "getUserResourcesDone": [Function],
+ "getUserResourcesInit": [Function],
"getUserSrmDone": [Function],
"getUserSrmInit": [Function],
},
@@ -223,9 +225,9 @@ Object {
"setEndDate": [Function],
"setReviewOpportunityType": [Function],
"setStartDate": [Function],
- "setSubtracks": [Function],
"setTags": [Function],
"setText": [Function],
+ "setTypes": [Function],
},
},
"errors": Object {
@@ -370,9 +372,10 @@ Object {
},
"tc": Object {
"COMPETITION_TRACKS": Object {
- "DATA_SCIENCE": "data_science",
- "DESIGN": "design",
- "DEVELOP": "develop",
+ "DATA_SCIENCE": "Data Science",
+ "DESIGN": "Design",
+ "DEVELOP": "Development",
+ "QA": "Quality Assurance",
},
"REVIEW_OPPORTUNITY_TYPES": Object {
"Contest Review": "Review",
diff --git a/__tests__/actions/auth.js b/__tests__/actions/auth.js
index eff03f17..a36ef7e3 100644
--- a/__tests__/actions/auth.js
+++ b/__tests__/actions/auth.js
@@ -1,17 +1,20 @@
-const MOCK_GROUPS_REQ_URL = 'https://api.topcoder-dev.com/v3/groups?memberId=12345&membershipType=user';
+const MOCK_GROUPS_REQ_URL = 'https://api.topcoder-dev.com/v5/groups?memberId=12345&membershipType=user';
const MOCK_PROFILE_REQ_URL = 'https://api.topcoder-dev.com/v3/members/username12345';
jest.mock('isomorphic-fetch', () => jest.fn(url => Promise.resolve({
+ ok: true,
json: () => {
let content;
switch (url) {
- case MOCK_GROUPS_REQ_URL: content = ['Group1', 'Group2']; break;
- case MOCK_PROFILE_REQ_URL: content = { userId: 12345 }; break;
+ case MOCK_GROUPS_REQ_URL:
+ content = ['Group1', 'Group2'];
+ break;
+ case MOCK_PROFILE_REQ_URL:
+ content = { result: { content: { userId: 12345 }, status: 200 } };
+ break;
default: throw new Error('Unexpected URL!');
}
- return {
- result: { content, status: 200 },
- };
+ return content;
},
})));
diff --git a/__tests__/utils/challenge/filter.js b/__tests__/utils/challenge/filter.js
index 950a1ca9..01a4753b 100644
--- a/__tests__/utils/challenge/filter.js
+++ b/__tests__/utils/challenge/filter.js
@@ -1,5 +1,5 @@
import {
- setText, setTags, setSubtracks, setStartDate,
+ setText, setTags, setTypes, setStartDate,
} from '../../../src/utils/challenge/filter';
describe('challenge filter', () => {
@@ -22,12 +22,12 @@ describe('challenge filter', () => {
expect(res).toEqual({});
});
- test('setSubtracks', () => {
- res = setSubtracks({});
+ test('setTypes', () => {
+ res = setTypes({});
expect(res).toEqual({});
- res = setSubtracks({}, 'subtracks');
- expect(res).toEqual({ subtracks: 'subtracks' });
- res = setSubtracks({ subtracks: 'subtracks' });
+ res = setTypes({}, 'types');
+ expect(res).toEqual({ types: 'types' });
+ res = setTypes({ types: 'types' });
expect(res).toEqual({});
});
diff --git a/config/test.js b/config/test.js
index add8c31c..1d2ca856 100644
--- a/config/test.js
+++ b/config/test.js
@@ -2,6 +2,7 @@ module.exports = {
API: {
V2: 'https://api.topcoder-dev.com/v2',
V3: 'https://api.topcoder-dev.com/v3',
+ V5: 'https://api.topcoder-dev.com/v5',
},
dummyConfigKey: 'Dummy config value',
SECRET: {
diff --git a/docs/challenge.filter.md b/docs/challenge.filter.md
index 978050ee..391ef5e5 100644
--- a/docs/challenge.filter.md
+++ b/docs/challenge.filter.md
@@ -70,7 +70,7 @@ users are participating.
* [.setEndDate(state, date)](#module_challenge.filter.setEndDate) ⇒ Object
* [.setReviewOpportunityType(state, reviewOpportunityType)](#module_challenge.filter.setReviewOpportunityType) ⇒ Object
* [.setStartDate(state, date)](#module_challenge.filter.setStartDate) ⇒ Object
- * [.setSubtracks(state, subtracks)](#module_challenge.filter.setSubtracks) ⇒ Object
+ * [.setTypes(state, types)](#module_challenge.filter.setTypes) ⇒ Object
* [.setTags(state, tags)](#module_challenge.filter.setTags) ⇒ Object
* [.setText(state, text)](#module_challenge.filter.setText) ⇒ Object
* _inner_
@@ -198,17 +198,17 @@ Clones the state and sets the start date.
| state | Object
| |
| date | String
| ISO date string. |
-
+
-### challenge.filter.setSubtracks(state, subtracks) ⇒ Object
-Clones the state and sets the subtracks.
+### challenge.filter.setTypes(state, types) ⇒ Object
+Clones the state and sets the challenge types.
**Kind**: static method of [challenge.filter
](#module_challenge.filter)
| Param | Type |
| --- | --- |
| state | Object
|
-| subtracks | Array
|
+| types | Array
|
diff --git a/package-lock.json b/package-lock.json
index 503042cf..5728771f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "topcoder-react-lib",
- "version": "1000.19.2",
+ "version": "1000.19.39",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index ae0c3eb7..35c82bc4 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
"lint:js": "./node_modules/.bin/eslint --ext .js,.jsx .",
"test": "npm run lint && npm run jest"
},
- "version": "1000.19.22",
+ "version": "1000.19.51",
"dependencies": {
"auth0-js": "^6.8.4",
"config": "^3.2.0",
diff --git a/src/actions/auth.js b/src/actions/auth.js
index c585ec71..f758d9ed 100644
--- a/src/actions/auth.js
+++ b/src/actions/auth.js
@@ -5,7 +5,7 @@
import { createActions } from 'redux-actions';
import { decodeToken } from 'tc-accounts';
-import { getApi } from '../services/api';
+import { getApiV3, getApiV5 } from '../services/api';
/**
* @static
@@ -16,12 +16,14 @@ import { getApi } from '../services/api';
function loadProfileDone(userTokenV3) {
if (!userTokenV3) return Promise.resolve(null);
const user = decodeToken(userTokenV3);
- const api = getApi('V3', userTokenV3);
+ const apiV3 = getApiV3(userTokenV3);
+ const apiV5 = getApiV5(userTokenV3);
return Promise.all([
- api.get(`/members/${user.handle}`)
+ apiV3.get(`/members/${user.handle}`)
.then(res => res.json()).then(res => (res.result.status === 200 ? res.result.content : {})),
- api.get(`/groups?memberId=${user.userId}&membershipType=user`)
- .then(res => res.json()).then(res => (res.result.status === 200 ? res.result.content : [])),
+ apiV5.get(`/groups?memberId=${user.userId}&membershipType=user`)
+ .then(res => (res.ok ? res.json() : new Error(res.statusText)))
+ .then(res => (res.message ? new Error(res.message) : res)),
]).then(([profile, groups]) => ({ ...profile, groups }));
}
diff --git a/src/actions/challenge.js b/src/actions/challenge.js
index 4627c0a3..c8fa4ace 100644
--- a/src/actions/challenge.js
+++ b/src/actions/challenge.js
@@ -7,6 +7,7 @@
import _ from 'lodash';
import { config } from 'topcoder-react-utils';
import { createActions } from 'redux-actions';
+import { decodeToken } from 'tc-accounts';
import { getService as getChallengesService } from '../services/challenges';
import { getService as getSubmissionService } from '../services/submissions';
import { getService as getMemberService } from '../services/members';
@@ -103,16 +104,20 @@ function getSubmissionsInit(challengeId) {
* @desc Creates an action that loads user's submissions to the specified
* challenge.
* @param {String} challengeId Challenge ID.
- * @param {String} tokenV2 Topcoder auth token v2.
+ * @param {String} tokenV3 Topcoder auth token v3.
* @return {Action}
*/
-function getSubmissionsDone(challengeId, tokenV2) {
- return getApi('V2', tokenV2)
- .fetch(`/challenges/submissions/${challengeId}/mySubmissions`)
- .then(response => response.json())
- .then(response => ({
+function getSubmissionsDone(challengeId, tokenV3) {
+ const user = decodeToken(tokenV3);
+ const submissionsService = getSubmissionService(tokenV3);
+ const filters = {
+ challengeId,
+ memberId: user.userId,
+ };
+ return submissionsService.getSubmissions(filters)
+ .then(submissions => ({
challengeId: _.toString(challengeId),
- submissions: response.submissions,
+ submissions,
}))
.catch((error) => {
const err = { challengeId: _.toString(challengeId), error };
@@ -289,13 +294,13 @@ function fetchCheckpointsDone(tokenV2, challengeId) {
response.checkpointResults[index].expanded = false;
});
return {
- challengeId: Number(challengeId),
+ challengeId: String(challengeId),
checkpoints: response,
};
})
.catch(error => ({
error,
- challengeId: Number(challengeId),
+ challengeId: String(challengeId),
}));
}
diff --git a/src/actions/members.js b/src/actions/members.js
index ac7bd701..13a14fdb 100644
--- a/src/actions/members.js
+++ b/src/actions/members.js
@@ -357,6 +357,30 @@ async function getUserMarathonDone(
}));
}
+/**
+ * @static
+ * @desc Create an action that fetch user registered challenge's resources.
+ * @param {String} memberId Member id.
+ * @param {String} uuid Operation UUID.
+ * @return {Action}
+ */
+async function getUserResourcesInit(memberId, uuid) {
+ return { memberId, uuid };
+}
+
+/**
+ * @static
+ * @desc Create an action that fetch user registered challenge's resources.
+ * @param {String} handle Member handle.
+ * @param {String} uuid Operation UUID.
+ * @return {Action}
+ */
+async function getUserResourcesDone(memberId, tokenV3, uuid) {
+ const resources = await getService(tokenV3).getUserResources(memberId);
+
+ return { resources, uuid };
+}
+
export default createActions({
MEMBERS: {
DROP: drop,
@@ -380,5 +404,7 @@ export default createActions({
GET_USER_SRM_DONE: getUserSRMDone,
GET_USER_MARATHON_INIT: getUserMarathonInit,
GET_USER_MARATHON_DONE: getUserMarathonDone,
+ GET_USER_RESOURCES_INIT: getUserResourcesInit,
+ GET_USER_RESOURCES_DONE: getUserResourcesDone,
},
});
diff --git a/src/actions/smp.js b/src/actions/smp.js
index 9c46f513..bded6d70 100644
--- a/src/actions/smp.js
+++ b/src/actions/smp.js
@@ -22,7 +22,7 @@ function deleteSubmissionInit() {}
* @return {Action}
*/
function deleteSubmissionDone(tokenV3, submissionId) {
- return getApi('V3', tokenV3).delete(`/submissions/${submissionId}`)
+ return getApi('V5', tokenV3).delete(`/submissions/${submissionId}`)
.then(() => submissionId);
}
diff --git a/src/actions/terms.js b/src/actions/terms.js
index 136307df..bc0a1907 100644
--- a/src/actions/terms.js
+++ b/src/actions/terms.js
@@ -32,7 +32,7 @@ function getTermsInit(arg) {
* @return {Action}
*/
function getTermsDone(entity, tokens, mockAgreed) {
- const service = getService(tokens.tokenV2);
+ const service = getService(tokens.tokenV3);
let termsPromise;
// if mockAgreed=true passed, then we create an array of 10 true which we pass to the
@@ -44,7 +44,7 @@ function getTermsDone(entity, tokens, mockAgreed) {
switch (entity.type) {
case 'challenge': {
- termsPromise = service.getChallengeTerms(entity.id, mockAgreedArray);
+ termsPromise = service.getChallengeTerms(entity.terms, mockAgreedArray);
break;
}
case 'community': {
@@ -59,7 +59,7 @@ function getTermsDone(entity, tokens, mockAgreed) {
throw new Error(`Entity type '${entity.type}' is not supported by getTermsDone.`);
}
- return termsPromise.then(res => ({ entity, terms: res.terms }));
+ return termsPromise.then(res => ({ entity, terms: res }));
}
/**
@@ -152,11 +152,11 @@ function getTermDetailsInit(termId) {
* @static
* @desc Creates an action that fetches details of the specified term.
* @param {Number|String} termId
- * @param {String} tokenV2
+ * @param {String} tokenV3
* @return {Action}
*/
-function getTermDetailsDone(termId, tokenV2) {
- const service = getService(tokenV2);
+function getTermDetailsDone(termId, tokenV3) {
+ const service = getService(tokenV3);
return service.getTermDetails(termId).then(details => ({ termId, details }));
}
@@ -175,11 +175,11 @@ function getDocuSignUrlInit(templateId) {
* @desc Creates an action that generates the url of DoduSign term
* @param {Number|String} templateId id of document template to sign
* @param {String} returnUrl callback url after finishing singing
- * @param {String} tokenV2 auth token
+ * @param {String} tokenV3 auth token
* @return {Action}
*/
-function getDocuSignUrlDone(templateId, returnUrl, tokenV2) {
- const service = getService(tokenV2);
+function getDocuSignUrlDone(templateId, returnUrl, tokenV3) {
+ const service = getService(tokenV3);
return service.getDocuSignUrl(templateId, returnUrl)
.then(resp => ({ templateId, docuSignUrl: resp.recipientViewUrl }));
}
@@ -198,11 +198,11 @@ function agreeTermInit(termId) {
* @static
* @desc Creates an action that agrees to a term.
* @param {Number|String} termId id of term
- * @param {String} tokenV2 auth token
+ * @param {String} tokenV3 auth token
* @return {Action}
*/
-function agreeTermDone(termId, tokenV2) {
- const service = getService(tokenV2);
+function agreeTermDone(termId, tokenV3) {
+ const service = getService(tokenV3);
return service.agreeTerm(termId).then(resp => ({ termId, success: resp.success }));
}
diff --git a/src/reducers/challenge.js b/src/reducers/challenge.js
index 6db8365f..7fb1dbd9 100644
--- a/src/reducers/challenge.js
+++ b/src/reducers/challenge.js
@@ -18,6 +18,8 @@ import { fireErrorMessage } from '../utils/errors';
import mySubmissionsManagement from './my-submissions-management';
+import { COMPETITION_TRACKS } from '../utils/tc';
+
/**
* Handles CHALLENGE/GET_DETAILS_INIT action.
* @param {Object} state
@@ -171,7 +173,7 @@ function onFetchCheckpointsDone(state, action) {
loadingCheckpoints: false,
};
}
- if (state.details && state.details.id === action.payload.challengeId) {
+ if (state.details && state.details.legacyId === action.payload.challengeId) {
return {
...state,
checkpoints: action.payload.checkpoints,
@@ -465,15 +467,25 @@ export function factory(options = {}) {
challengeId,
tokens.tokenV3,
tokens.tokenV2,
- )).then((details) => {
- const track = _.get(details, 'payload.track', '').toLowerCase();
- const checkpointsPromise = track === 'design' ? (
- redux.resolveAction(actions.challenge.fetchCheckpointsDone(tokens.tokenV2, challengeId))
- ) : null;
- const resultsPromise = _.get(details, 'payload.status', '') === 'Completed' ? (
- redux.resolveAction(actions.challenge.loadResultsDone(tokens, challengeId, track))
+ )).then((res) => {
+ const challengeDetails = _.get(res, 'payload', {});
+ const track = _.get(challengeDetails, 'track', '');
+ let checkpointsPromise = null;
+ if (track === COMPETITION_TRACKS.DESIGN) {
+ const p = _.get(challengeDetails, 'phases', [])
+ .filter(x => x.name === 'Checkpoint Review');
+ if (p.length && !p[0].isOpen) {
+ checkpointsPromise = redux.resolveAction(
+ actions.challenge.fetchCheckpointsDone(tokens.tokenV2, challengeDetails.legacyId),
+ );
+ }
+ }
+ const resultsPromise = challengeDetails.status === 'Completed' ? (
+ redux.resolveAction(
+ actions.challenge.loadResultsDone(tokens, challengeId, track.toLowerCase()),
+ )
) : null;
- return Promise.all([details, checkpointsPromise, resultsPromise]);
+ return Promise.all([res, checkpointsPromise, resultsPromise]);
}).then(([details, checkpoints, results]) => {
state = {
...state,
diff --git a/src/reducers/members.js b/src/reducers/members.js
index 23d0a08b..76c70d52 100644
--- a/src/reducers/members.js
+++ b/src/reducers/members.js
@@ -426,6 +426,43 @@ function onGetUserMarathonDone(state, { error, payload }) {
};
}
+/**
+ * Inits the loading of user challenge resources.
+ * @param {Object} state
+ * @param {Object} action
+ * @return {Object} New state.
+ */
+function onGetUserResourcesInit(state, { payload }) {
+ const { uuid } = payload;
+ return {
+ ...state,
+ userResources: { resources: [], loadingUserResources: uuid },
+ };
+}
+
+/**
+ * Finalizes the loading of user challenge resources.
+ * @param {Object} state
+ * @param {Object} action
+ * @return {Object} New state.
+ */
+function onGetUserResourcesDone(state, { error, payload }) {
+ if (error) {
+ logger.error('Failed to get user resources', payload);
+ fireErrorMessage('Failed to get user resources', '');
+ return state;
+ }
+
+ const { uuid, resources } = payload;
+
+ if (uuid !== state.userResources.loadingUserResources) return state;
+
+ return {
+ ...state,
+ userResources: { resources, loadingUserResources: '' },
+ };
+}
+
/**
* Creates a new Members reducer with the specified initial state.
* @param {Object} initialState Optional. Initial state.
@@ -455,6 +492,8 @@ function create(initialState = {}) {
[a.getUserSrmDone]: onGetUserSRMDone,
[a.getUserMarathonInit]: onGetUserMarathonInit,
[a.getUserMarathonDone]: onGetUserMarathonDone,
+ [a.getUserResourcesInit]: onGetUserResourcesInit,
+ [a.getUserResourcesDone]: onGetUserResourcesDone,
}, initialState);
}
diff --git a/src/reducers/reviewOpportunity.js b/src/reducers/reviewOpportunity.js
index 0bea4116..e1b8d5b8 100644
--- a/src/reducers/reviewOpportunity.js
+++ b/src/reducers/reviewOpportunity.js
@@ -23,8 +23,8 @@ function buildRequiredTermsList(details) {
// Sometimes roles such as Primary Reviewer have no directly equal
// terms entry. Include the plain Reviewer terms when present as a back-up.
.filter(term => term.role === 'Reviewer' || _.includes(roles, term.role))
- .map(term => _.pick(term, ['termsOfUseId', 'agreed', 'title'])),
- term => term.termsOfUseId,
+ .map(term => _.pick(term, ['id', 'agreed', 'title'])),
+ term => term.id,
);
return requiredTerms || [];
diff --git a/src/reducers/terms.js b/src/reducers/terms.js
index b6bfdb49..d951643e 100644
--- a/src/reducers/terms.js
+++ b/src/reducers/terms.js
@@ -181,7 +181,7 @@ function onAgreeTermDone(state, action) {
}
if (action.payload.success) {
const terms = _.cloneDeep(state.terms);
- const term = _.find(terms, ['termsOfUseId', action.payload.termId]);
+ const term = _.find(terms, ['id', action.payload.termId]);
term.agreed = true;
const selectedTerm = _.find(terms, t => !t.agreed);
return {
diff --git a/src/services/__mocks__/challenges.js b/src/services/__mocks__/challenges.js
index 0dc59e7c..2c5e7ae4 100644
--- a/src/services/__mocks__/challenges.js
+++ b/src/services/__mocks__/challenges.js
@@ -85,8 +85,8 @@ export function normalizeChallengeDetails(v3, v3Filtered, v3User, v2, username)
// Fill missing data from v3_filtered
if (v3Filtered) {
const groups = {};
- if (v3Filtered.groupIds) {
- v3Filtered.groupIds.forEach((id) => {
+ if (v3Filtered.groups) {
+ v3Filtered.groups.forEach((id) => {
groups[id] = true;
});
}
@@ -165,8 +165,8 @@ export function normalizeChallengeDetails(v3, v3Filtered, v3User, v2, username)
export function normalizeChallenge(challenge, username) {
const registrationOpen = challenge.allPhases.filter(d => d.name === 'Registration')[0].isOpen ? 'Yes' : 'No';
const groups = {};
- if (challenge.groupIds) {
- challenge.groupIds.forEach((id) => {
+ if (challenge.groups) {
+ challenge.groups.forEach((id) => {
groups[id] = true;
});
}
diff --git a/src/services/challenges.js b/src/services/challenges.js
index ea3818cb..fd0fecec 100644
--- a/src/services/challenges.js
+++ b/src/services/challenges.js
@@ -53,18 +53,13 @@ export function normalizeChallenge(challenge, username) {
if (!challenge.tags) challenge.tags = [];
if (!challenge.platforms) challenge.platforms = [];
- if (challenge.type === 'Marathon Match') {
- challenge.legacy.track = 'DATA_SCIENCE';
- }
- /* eslint-enable no-param-reassign */
-
let submissionEndTimestamp = phases.filter(d => d.name === 'Submission')[0];
if (submissionEndTimestamp) {
submissionEndTimestamp = submissionEndTimestamp.scheduledEndDate;
}
const prizes = (challenge.prizeSets[0] && challenge.prizeSets[0].prizes) || [];
_.defaults(challenge, {
- communities: new Set([COMPETITION_TRACKS[challenge.legacy.track]]),
+ communities: new Set([COMPETITION_TRACKS[challenge.track]]),
groups,
registrationOpen,
submissionEndTimestamp,
@@ -327,6 +322,7 @@ class ChallengesService {
let submissions = [];
let isLegacyChallenge = false;
let isRegistered = false;
+ const userDetails = { roles: [] };
// condition based on ROUTE used for Review Opportunities, change if needed
if (/^[\d]{5,8}$/.test(challengeId)) {
@@ -348,32 +344,31 @@ class ChallengesService {
/* Prepare data to logged user */
if (memberId) {
- isRegistered = _.some(registrants, r => r.memberId === memberId);
+ isRegistered = _.some(registrants, r => `${r.memberId}` === `${memberId}`);
- /**
- * TODO: Currenlty using legacyId until submissions_api fix issue with UUID
- */
const subParams = {
- challengeId: challenge.legacyId,
+ challengeId,
perPage: 100,
};
+
submissions = await this.private.submissionsService.getSubmissions(subParams);
if (submissions) {
// Remove AV Scan, SonarQube Review and Virus Scan review types
const reviewScans = await this.private.submissionsService.getScanReviewIds();
submissions.forEach((s, i) => {
- submissions[i].review = _.reject(s.review, r => _.includes(reviewScans, r.typeId));
+ submissions[i].review = _.reject(s.review, r => r && _.includes(reviewScans, r.typeId));
});
// Add submission date to registrants
registrants.forEach((r, i) => {
- const submission = submissions.find(s => s.memberId === Number(r.memberId));
+ const submission = submissions.find(s => `${s.memberId}` === `${r.memberId}`);
if (submission) {
registrants[i].submissionDate = submission.created;
}
});
}
+ userDetails.roles = await this.getUserRolesInChallenge(challengeId);
}
challenge = {
@@ -382,6 +377,7 @@ class ChallengesService {
isRegistered,
registrants,
submissions,
+ userDetails,
events: _.map(challenge.events, e => ({
eventName: e.key,
eventId: e.id,
@@ -401,14 +397,20 @@ class ChallengesService {
*/
async getChallengeRegistrants(challengeId) {
/* If no token provided, resource will return Submitter role only */
+ const roleId = this.private.tokenV3 ? await this.getRoleId('Submitter') : '';
const params = {
challengeId,
- roleId: this.private.tokenV3 ? await this.getRoleId('Submitter') : '',
+ roleId,
};
- const registrants = await this.private.apiV5.get(`/resources?${qs.stringify(params)}`)
+ let registrants = await this.private.apiV5.get(`/resources?${qs.stringify(params)}`)
.then(checkErrorV5).then(res => res.result);
+ /* API will return all roles to currentUser, so need to filter in FE */
+ if (roleId) {
+ registrants = _.filter(registrants, r => r.roleId === roleId);
+ }
+
return registrants || [];
}
@@ -517,6 +519,16 @@ class ChallengesService {
};
}
+ /**
+ * Gets user resources.
+ * @param {String} userId User id whose challenges we want to fetch.
+ * @return {Promise} Resolves to the api response.
+ */
+ async getUserResources(userId) {
+ const res = await this.private.apiV5.get(`/resources/${userId}/challenges`);
+ return res.json();
+ }
+
/**
* Gets marathon matches of the specified user.
* @param {String} memberId User whose challenges we want to fetch.
@@ -524,15 +536,9 @@ class ChallengesService {
* @return {Promise} Resolves to the api response.
*/
async getUserMarathonMatches(memberId, params) {
- const typeId = await this.getChallengeTypeId('DEVELOP_MARATHON_MATCH');
-
- if (!typeId) {
- return null;
- }
-
const newParams = {
...params,
- typeId,
+ tag: 'Marathon Match',
memberId,
};
@@ -632,7 +638,7 @@ class ChallengesService {
let contentType;
let url;
- if (track === 'DESIGN') {
+ if (track === COMPETITION_TRACKS.DESIGN) {
({ api } = this.private);
contentType = 'application/json';
url = '/submissions/'; // The submission info is contained entirely in the JSON body
@@ -650,7 +656,7 @@ class ChallengesService {
}, onProgress).then((res) => {
const jres = JSON.parse(res);
// Return result for Develop submission
- if (track === 'DEVELOP') {
+ if (track === COMPETITION_TRACKS.DEVELOP) {
return jres;
}
// Design Submission requires an extra "Processing" POST
@@ -695,9 +701,10 @@ class ChallengesService {
*/
async getUserRolesInChallenge(challengeId) {
const user = decodeToken(this.private.tokenV3);
- const url = `/resources?challengeId=${challengeId}?memberHandle=${user.handle}`;
- const resources = await this.private.apiV5.get(url);
- if (resources) return _.map(resources, 'roleId');
+ const url = `/resources?challengeId=${challengeId}&memberHandle=${user.handle}`;
+ const getResourcesResponse = await this.private.apiV5.get(url);
+ const resources = await getResourcesResponse.json();
+ if (resources) return _.map(_.filter(resources, r => r.memberHandle === user.handle), 'roleId');
throw new Error(`Failed to fetch user role from challenge #${challengeId}`);
}
}
diff --git a/src/services/groups.js b/src/services/groups.js
index 69603c0f..a3b42a80 100644
--- a/src/services/groups.js
+++ b/src/services/groups.js
@@ -170,8 +170,8 @@ function mergeGroup(groups, group) {
* @param {Object} group
* @return {String[]} Array of IDs.
*/
-export function reduceGroupIds({ oldId, subGroups }) {
- let res = [oldId];
+export function reduceGroupIds({ id, subGroups }) {
+ let res = [id];
if (subGroups) {
subGroups.forEach((g) => {
res = res.concat(reduceGroupIds(g));
@@ -210,7 +210,7 @@ class GroupService {
*/
async addMember(groupId, memberId, membershipType) {
const response = await this.private.api.postJson(`/groups/${groupId}/members`, {
- param: { memberId, membershipType },
+ memberId, membershipType,
});
return handleApiResponse(response);
diff --git a/src/services/members.js b/src/services/members.js
index 128a61f4..a5b32a51 100644
--- a/src/services/members.js
+++ b/src/services/members.js
@@ -7,6 +7,7 @@
/* global XMLHttpRequest */
import _ from 'lodash';
import qs from 'qs';
+import { decodeToken } from 'tc-accounts';
import logger from '../utils/logger';
import { getApiResponsePayload } from '../utils/tc';
import { getApi } from './api';
@@ -21,6 +22,7 @@ class MembersService {
constructor(tokenV3) {
this.private = {
api: getApi('V3', tokenV3),
+ apiV5: getApi('V5', tokenV3),
tokenV3,
};
}
@@ -312,6 +314,64 @@ class MembersService {
const res = await this.private.api.get(url);
return getApiResponsePayload(res);
}
+
+ /**
+ * Fetch resources roles
+ * @param {Array} memberId the member id
+ */
+ async getResourceRoles() {
+ const res = await this.private.apiV5.get('/resource-roles');
+ const roles = await res.json();
+ return roles;
+ }
+
+ /**
+ * Fetch user challenge resources
+ * @param {Array} challengeId the challenge id
+ */
+ async getChallengeResources(challengeId) {
+ const user = decodeToken(this.private.tokenV3);
+ const url = `/resources?challengeId=${challengeId}&memberId=${user.userId}`;
+ let res = null;
+
+ try {
+ res = await this.private.apiV5.get(url);
+ } catch (error) {
+ // logger.error('Failed to load challenge resource', error);
+ }
+
+ return res.json();
+ }
+
+ /**
+ * Fetch user registered challenge's resources
+ * @param {Array} memberId the member id
+ */
+ async getUserResources(memberId) {
+ const url = `/challenges?status=Active&memberId=${memberId}`;
+ const res = await this.private.apiV5.get(url);
+ const challenges = await res.json();
+ const roles = await this.getResourceRoles();
+ const calls = [];
+
+ challenges.forEach(async (ch) => {
+ calls.push(this.getChallengeResources(ch.id));
+ });
+
+ return Promise.all(calls).then((resources) => {
+ const results = [];
+ resources.forEach((resource) => {
+ const userResource = _.find(resource, { memberId });
+ if (userResource) {
+ const challengeRole = _.find(roles, { id: userResource.roleId });
+ const { name } = challengeRole || '';
+ results.push({ id: userResource.challengeId, name });
+ }
+ });
+
+ return results;
+ });
+ }
}
let lastInstance = null;
diff --git a/src/services/reviewOpportunities.js b/src/services/reviewOpportunities.js
index 42ad4844..51af9e44 100644
--- a/src/services/reviewOpportunities.js
+++ b/src/services/reviewOpportunities.js
@@ -22,6 +22,22 @@ export function normalizeChallenges(challenges) {
}
return challenges;
}
+
+/**
+ * Sync the fields of V3 and V5 for front-end to process successfully
+ * @param challenge - challenge to normalize
+ */
+function normalizeChallengePhases(challenge) {
+ return {
+ ...challenge,
+ phases: _.map(challenge.phases, phase => ({
+ ...phase,
+ scheduledStartDate: phase.scheduledStartTime,
+ scheduledEndDate: phase.scheduledEndTime,
+ })),
+ };
+}
+
/**
* Service class.
*/
@@ -64,8 +80,10 @@ class ReviewOpportunitiesService {
.then(res => res.json())
.then(res => (
res.result.status === 200
- ? res.result.content
- : Promise.reject(res.result)
+ ? {
+ ...res.result.content,
+ challenge: normalizeChallengePhases(res.result.content.challenge),
+ } : Promise.reject(res.result)
));
}
diff --git a/src/services/submissions.js b/src/services/submissions.js
index 36e78fb6..12f27021 100644
--- a/src/services/submissions.js
+++ b/src/services/submissions.js
@@ -5,8 +5,32 @@
*/
import _ from 'lodash';
import qs from 'qs';
+import { setErrorIcon, ERROR_ICON_TYPES } from '../utils/errors';
import { getApi } from './api';
+/**
+ * Helper method that checks for HTTP error response v5 and throws Error in this case.
+ * @param {Object} res HTTP response object
+ * @return {Object} API JSON response object
+ * @private
+ */
+async function checkErrorV5(res) {
+ if (!res.ok) {
+ if (res.status >= 500) {
+ setErrorIcon(ERROR_ICON_TYPES.API, '/challenges', res.statusText);
+ }
+ throw new Error(res.statusText);
+ }
+ const jsonRes = (await res.json());
+ if (jsonRes.message) {
+ throw new Error(res.message);
+ }
+ return {
+ result: jsonRes,
+ headers: res.headers,
+ };
+}
+
/**
* Submission service.
*/
@@ -36,8 +60,8 @@ class SubmissionsService {
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);
+ .then(checkErrorV5)
+ .then(res => res.result);
}
/**
@@ -47,14 +71,14 @@ class SubmissionsService {
async getScanReviewIds() {
const reviews = await Promise.all([
this.private.apiV5.get('/reviewTypes?name=AV Scan')
- .then(res => (res.ok ? res.json() : new Error(res.statusText)))
- .then(res => res),
+ .then(checkErrorV5)
+ .then(res => res.result),
this.private.apiV5.get('/reviewTypes?name=SonarQube Review')
- .then(res => (res.ok ? res.json() : new Error(res.statusText)))
- .then(res => res),
+ .then(checkErrorV5)
+ .then(res => res.result),
this.private.apiV5.get('/reviewTypes?name=Virus Scan')
- .then(res => (res.ok ? res.json() : new Error(res.statusText)))
- .then(res => res),
+ .then(checkErrorV5)
+ .then(res => res.result),
]).then(([av, sonar, virus]) => (_.concat(av, sonar, virus)));
return reviews.map(r => r.id);
diff --git a/src/services/terms.js b/src/services/terms.js
index b34e62e1..224f825a 100644
--- a/src/services/terms.js
+++ b/src/services/terms.js
@@ -8,6 +8,7 @@ import _ from 'lodash';
import { config } from 'topcoder-react-utils';
import { getService as getCommunityService } from './communities';
+import { getService as getChallengeService } from './challenges';
import { getApi } from './api';
/**
@@ -15,54 +16,33 @@ import { getApi } from './api';
*/
class TermsService {
/**
- * @param {String} tokenV2 Optional. Auth token for Topcoder API v2.
+ * @param {String} tokenV3 Optional. Auth token for Topcoder API v3.
*/
- constructor(tokenV2) {
+ constructor(tokenV3) {
this.private = {
- api: getApi('V2', tokenV2),
- tokenV2,
+ api: getApi('V5', tokenV3),
+ tokenV3,
};
}
/**
* get all terms of specified challenge
- * @param {Number|String} challengeId id of the challenge
+ * @param {Array} terms terms of the challenge
* @return {Promise} promise of the request result
*/
- getChallengeTerms(challengeId) {
- if (this.private.tokenV2) {
- let registered = false;
- return this.private.api.get(`/terms/${challengeId}?role=Submitter`)
- .then(res => res.json())
- .then((res) => {
- if (res.error) {
- if (res.error.details === 'You are already registered for this challenge.') {
- registered = true;
- }
- return this.private.api.get(`/terms/${challengeId}?role=Submitter&noauth=true`)
- .then((resp) => {
- if (resp.ok) {
- return resp.json().then((result) => {
- if (registered) {
- // eslint-disable-next-line no-param-reassign
- _.forEach(result.terms, (t) => { t.agreed = true; });
- }
- return result;
- });
- }
- return new Error(resp.statusText);
- });
- }
- return res;
- });
+ async getChallengeTerms(terms) {
+ if (this.private.tokenV3) {
+ const challengeService = getChallengeService(this.private.tokenV3);
+ const roleId = await challengeService.getRoleId('Submitter');
+ const registerTerms = _.filter(terms, t => t.roleId === roleId);
+
+ return Promise.all(_.map(registerTerms, term => this.getTermDetails(term.id)))
+ .then(challengeTerms => (
+ _.map(challengeTerms, term => _.pick(term, 'id', 'title', 'agreed'))
+ ));
}
- return this.private.api.get(`/terms/${challengeId}?role=Submitter&noauth=true`)
- .then((resp) => {
- if (resp.ok) {
- return resp.json();
- }
- throw new Error(resp.statusText);
- });
+
+ return [];
}
/**
@@ -88,9 +68,7 @@ class TermsService {
}
return [];
- }).then(terms => ({
- terms,
- }));
+ }).then(terms => terms);
}
/**
@@ -110,10 +88,10 @@ class TermsService {
return Promise.resolve(term);
}
// Otherwise grab new details from terms api
- return this.getTermDetails(term.termsOfUseId).then(res => _.pick(res, ['termsOfUseId', 'agreed', 'title']));
+ return this.getTermDetails(term.id).then(res => _.pick(res, ['id', 'agreed', 'title']));
});
- return Promise.all(promises).then(terms => ({ terms }));
+ return Promise.all(promises).then(terms => terms);
}
/**
@@ -123,8 +101,7 @@ class TermsService {
*/
getTermDetails(termId) {
// looks like server cache responses, to prevent it we add nocache param with always new value
- const nocache = (new Date()).getTime();
- return this.private.api.get(`/terms/detail/${termId}?nocache=${nocache}`)
+ return this.private.api.get(`/terms/${termId}`)
.then(res => (res.ok ? res.json() : Promise.reject(res.json())));
}
@@ -135,7 +112,11 @@ class TermsService {
* @return {Promise} promise of the request result
*/
getDocuSignUrl(templateId, returnUrl) {
- return this.private.api.post(`/terms/docusign/viewURL?templateId=${templateId}&returnUrl=${returnUrl}`)
+ const params = {
+ templateId,
+ returnUrl,
+ };
+ return this.private.api.postJson('/terms/docusignViewURL', params)
.then(res => (res.ok ? res.json() : Promise.reject(res.json())));
}
@@ -153,20 +134,20 @@ class TermsService {
let lastInstance = null;
/**
* Returns a new or existing terms service.
- * @param {String} tokenV2 Optional. Auth token for Topcoder API v2.
+ * @param {String} tokenV3 Optional. Auth token for Topcoder API v3.
* @return {TermsService} Terms service object
*/
-export function getService(tokenV2) {
+export function getService(tokenV3) {
/* Because of Topcoder backend restrictions, it is not straightforward to test
* terms-related functionality in any other way than just providing an option
* to run the app against mock terms service. */
if (config.MOCK_TERMS_SERVICE) {
/* eslint-disable global-require */
- return require('./__mocks__/terms').getService(tokenV2);
+ return require('./__mocks__/terms').getService(tokenV3);
/* eslint-enable global-require */
}
- if (!lastInstance || (tokenV2 && lastInstance.private.tokenV2 !== tokenV2)) {
- lastInstance = new TermsService(tokenV2);
+ if (!lastInstance || (tokenV3 && lastInstance.private.tokenV3 !== tokenV3)) {
+ lastInstance = new TermsService(tokenV3);
}
return lastInstance;
}
diff --git a/src/utils/challenge/filter.js b/src/utils/challenge/filter.js
index fcaf1924..263c3132 100644
--- a/src/utils/challenge/filter.js
+++ b/src/utils/challenge/filter.js
@@ -18,7 +18,7 @@
* endDate {Number|String} - Permits only those challenges with submission
* deadline before this date.
*
- * groupIds {Array} - Permits only the challenges belonging to at least one
+ * groups {Array} - Permits only the challenges belonging to at least one
* of the groups which IDs are presented as keys in this object.
*
* or {Object[]} - All other filter fields applied to the challenge with AND
@@ -89,7 +89,7 @@ function filterByRegistrationOpen(challenge, state) {
if (!registrationPhase || !registrationPhase.isOpen) {
return false;
}
- if (challenge.track === 'DESIGN') {
+ if (challenge.track === COMPETITION_TRACKS.DESIGN) {
const checkpointPhase = challengePhases.find(item => item.name === 'Checkpoint Submission')[0];
return !checkpointPhase || !checkpointPhase.isOpen;
}
@@ -130,6 +130,14 @@ function filterByStarted(challenge, state) {
return moment(challenge.registrationStartDate).isBefore(Date.now());
}
+function filterByOngoing(challenge, state) {
+ if (_.isUndefined(state.ongoing)) return true;
+ const registrationPhase = (challenge.phases || []).filter(d => d.name === 'Registration')[0];
+ const registrationEndDate = registrationPhase ? registrationPhase.scheduledEndDate
+ : challenge.registrationEndDate;
+ return moment(registrationEndDate).isBefore(Date.now());
+}
+
function filterByStatus(challenge, state) {
if (!state.status) return true;
return state.status.includes(challenge.status);
@@ -151,20 +159,12 @@ function filterByText(challenge, state) {
function filterByTrack(challenge, state) {
if (!state.tracks) return true;
-
- /* Development challenges having Data Science tech tag, still should be
- * included into data science track. */
- if (state.tracks[COMPETITION_TRACKS.DATA_SCIENCE]
- && _.includes(challenge.tags, 'Data Science')) {
- return true;
- }
-
- return _.keys(state.tracks).some(track => challenge.communities.has(track));
+ return _.keys(state.tracks).some(track => challenge.track === track);
}
-function filterBySubtracks(challenge, state) {
- if (!state.subtracks) return true;
- return state.subtracks.includes(challenge.typeId);
+function filterByTypes(challenge, state) {
+ if (!state.types) return true;
+ return state.types.includes(challenge.typeId);
}
function filterByUpcoming(challenge, state) {
@@ -173,8 +173,8 @@ function filterByUpcoming(challenge, state) {
}
function filterByUsers(challenge, state) {
- if (!state.users) return true;
- return state.users.find(user => challenge.users[user]);
+ if (!state.userChallenges) return true;
+ return state.userChallenges.find(ch => challenge.id === ch);
}
/**
@@ -214,11 +214,12 @@ export function getFilterFunction(state) {
&& filterByGroupIds(challenge, state)
&& filterByText(challenge, state)
&& filterByTags(challenge, state)
- && filterBySubtracks(challenge, state)
+ && filterByTypes(challenge, state)
&& filterByUsers(challenge, state)
&& filterByEndDate(challenge, state)
&& filterByStartDate(challenge, state)
&& filterByStarted(challenge, state)
+ && filterByOngoing(challenge, state)
&& filterByRegistrationOpen(challenge, state);
if (!test && state.or) {
let pos = 0;
@@ -236,9 +237,9 @@ export function getFilterFunction(state) {
* @param {Object} state
* @return {Function}
*/
-export function getReviewOpportunitiesFilterFunction(state, validSubtracks) {
+export function getReviewOpportunitiesFilterFunction(state, validTypes) {
return (opp) => {
- const newSubTrack = _.find(validSubtracks, { abbreviation: opp.challenge.subTrack }) || {};
+ const newType = _.find(validTypes, { name: opp.challenge.type }) || {};
// Review Opportunity objects have a challenge field which
// is largely compatible with many of the existing filter functions
@@ -248,12 +249,11 @@ export function getReviewOpportunitiesFilterFunction(state, validSubtracks) {
// This allows filterByText to search for Review Types and Challenge Titles
name: `${opp.challenge.title} ${REVIEW_OPPORTUNITY_TYPES[opp.type]}`,
registrationStartDate: opp.startDate, // startDate of Review, not Challenge
- subTrack: opp.challenge.subTrack || '', // Sometimes back-end doesn't return this field
submissionEndDate: opp.startDate, // Currently uses startDate for both date comparisons
communities: new Set([ // Used to filter by Track, and communities at a future date
opp.challenge.track.toLowerCase(),
]),
- typeId: newSubTrack.id,
+ typeId: newType.id,
tags: opp.challenge.technologies || [],
platforms: opp.challenge.platforms || [],
};
@@ -262,7 +262,7 @@ export function getReviewOpportunitiesFilterFunction(state, validSubtracks) {
filterByTrack(challenge, state)
&& filterByText(challenge, state)
&& filterByTags(challenge, state)
- && filterBySubtracks(challenge, state)
+ // && filterByTypes(challenge, state)
&& filterByEndDate(challenge, state)
&& filterByStartDate(challenge, state)
&& filterByReviewOpportunityType(opp, state)
@@ -343,7 +343,7 @@ export function combine(...filters) {
const res = {};
filters.forEach((filter) => {
combineEndDate(res, filter);
- combineArrayRules(res, filter, 'groupIds');
+ combineArrayRules(res, filter, 'groups');
/* TODO: The registrationOpen rule is just ignored for now. */
combineStartDate(res, filter);
combineArrayRules(res, filter, 'or', true);
@@ -379,15 +379,8 @@ export function combine(...filters) {
* @return {Object}
*/
export function mapToBackend(filter) {
- if (filter.or) return {};
-
const res = {};
- if (filter.groupIds) res.groupIds = filter.groupIds.join(',');
-
- /* NOTE: Right now the frontend challenge filter by tag works different,
- * it looks for matches in the challenge name OR in the techs / platforms. */
- // if (filter.tags) res.technologies = filter.tags.join(',');
-
+ if (filter.groups) res.groups = filter.groups;
return res;
}
@@ -453,16 +446,16 @@ export function setStartDate(state, date) {
}
/**
- * Clones the state and sets the subtracks.
+ * Clones the state and sets the challenge types.
* @param {Object} state
- * @param {Array} subtracks
+ * @param {Array} types
* @return {Object}
*/
-export function setSubtracks(state, subtracks) {
- if (subtracks && subtracks.length) return { ...state, subtracks };
- if (!state.subtracks) return state;
+export function setTypes(state, types) {
+ if (types && types.length) return { ...state, types };
+ if (!state.types) return state;
const res = _.clone(state);
- delete res.subtracks;
+ delete res.types;
return res;
}
diff --git a/src/utils/tc.js b/src/utils/tc.js
index aed187ca..5388d4bb 100644
--- a/src/utils/tc.js
+++ b/src/utils/tc.js
@@ -11,9 +11,10 @@
* uses upper-case literals to encode the tracks. At some point, we should
* update it in this code as well! */
export const COMPETITION_TRACKS = {
- DATA_SCIENCE: 'data_science',
- DESIGN: 'design',
- DEVELOP: 'develop',
+ DATA_SCIENCE: 'Data Science',
+ DESIGN: 'Design',
+ DEVELOP: 'Development',
+ QA: 'Quality Assurance',
};
/**