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', }; /**