Skip to content

Commit 55dd751

Browse files
authored
Merge pull request #292 from topcoder-platform/develop
v1.0.2 Release
2 parents 476a710 + e0cc722 commit 55dd751

File tree

6 files changed

+114
-55
lines changed

6 files changed

+114
-55
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Topcoder Challenge API
22

33
This microservice provides access and interaction with all sorts of Challenge data.
4+
## Devlopment status
5+
[![Total alerts](https://img.shields.io/lgtm/alerts/g/topcoder-platform/challenge-api.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/topcoder-platform/challenge-api/alerts/)[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/topcoder-platform/challenge-api.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/topcoder-platform/challenge-api/context:javascript)
46

57
### Deployment status
68
Dev: [![CircleCI](https://circleci.com/gh/topcoder-platform/challenge-api/tree/develop.svg?style=svg)](https://circleci.com/gh/topcoder-platform/challenge-api/tree/develop) Prod: [![CircleCI](https://circleci.com/gh/topcoder-platform/challenge-api/tree/master.svg?style=svg)](https://circleci.com/gh/topcoder-platform/challenge-api/tree/master)

app-constants.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
* App constants
33
*/
44
const UserRoles = {
5-
Admin: 'Administrator',
6-
Copilot: 'Copilot',
5+
Admin: 'administrator',
6+
Copilot: 'copilot',
7+
Manager: 'Connect Manager',
78
User: 'Topcoder User'
89
}
910

@@ -70,6 +71,11 @@ const challengeTracks = {
7071
QA: 'QA'
7172
}
7273

74+
const challengeTextSortField = {
75+
Name: 'name',
76+
TypeId: 'typeId'
77+
}
78+
7379
module.exports = {
7480
UserRoles,
7581
prizeSetTypes,
@@ -78,5 +84,6 @@ module.exports = {
7884
EVENT_ORIGINATOR,
7985
EVENT_MIME_TYPE,
8086
Topics,
81-
challengeTracks
87+
challengeTracks,
88+
challengeTextSortField
8289
}

docs/swagger.yaml

+22
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,28 @@ paths:
174174
type: array
175175
items:
176176
type: string
177+
- name: includeAllTags
178+
in: query
179+
description: >-
180+
Require all provided tags to be present on a challenge for a match
181+
required: false
182+
default: true
183+
type: boolean
184+
- name: events
185+
in: query
186+
description: >-
187+
Filter by multiple event keys (ie: tco21)
188+
required: false
189+
type: array
190+
items:
191+
type: number
192+
- name: includeAllEvents
193+
in: query
194+
description: >-
195+
Require all provided events to be present on a challenge for a match
196+
required: false
197+
default: true
198+
type: boolean
177199
- name: projectId
178200
in: query
179201
description: 'Filter by v5 project id, exact match.'

src/routes.js

+16-16
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ module.exports = {
1616
get: {
1717
controller: 'ChallengeController',
1818
method: 'searchChallenges',
19-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.User],
19+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager, constants.UserRoles.User],
2020
scopes: [READ, ALL]
2121
},
2222
post: {
2323
controller: 'ChallengeController',
2424
method: 'createChallenge',
2525
auth: 'jwt',
26-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
26+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
2727
scopes: [CREATE, ALL]
2828
}
2929
},
@@ -43,14 +43,14 @@ module.exports = {
4343
controller: 'ChallengeController',
4444
method: 'fullyUpdateChallenge',
4545
auth: 'jwt',
46-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
46+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
4747
scopes: [UPDATE, ALL]
4848
},
4949
patch: {
5050
controller: 'ChallengeController',
5151
method: 'partiallyUpdateChallenge',
5252
auth: 'jwt',
53-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
53+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
5454
scopes: [UPDATE, ALL]
5555
}
5656
},
@@ -63,7 +63,7 @@ module.exports = {
6363
controller: 'ChallengeTypeController',
6464
method: 'createChallengeType',
6565
auth: 'jwt',
66-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
66+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
6767
scopes: [CREATE, ALL]
6868
}
6969
},
@@ -76,14 +76,14 @@ module.exports = {
7676
controller: 'ChallengeTypeController',
7777
method: 'fullyUpdateChallengeType',
7878
auth: 'jwt',
79-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
79+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
8080
scopes: [UPDATE, ALL]
8181
},
8282
patch: {
8383
controller: 'ChallengeTypeController',
8484
method: 'partiallyUpdateChallengeType',
8585
auth: 'jwt',
86-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
86+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
8787
scopes: [UPDATE, ALL]
8888
}
8989
},
@@ -96,7 +96,7 @@ module.exports = {
9696
controller: 'ChallengeTrackController',
9797
method: 'createChallengeTrack',
9898
auth: 'jwt',
99-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
99+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
100100
scopes: [CREATE, ALL]
101101
}
102102
},
@@ -109,14 +109,14 @@ module.exports = {
109109
controller: 'ChallengeTrackController',
110110
method: 'fullyUpdateChallengeTrack',
111111
auth: 'jwt',
112-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
112+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
113113
scopes: [UPDATE, ALL]
114114
},
115115
patch: {
116116
controller: 'ChallengeTrackController',
117117
method: 'partiallyUpdateChallengeTrack',
118118
auth: 'jwt',
119-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
119+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
120120
scopes: [UPDATE, ALL]
121121
}
122122
},
@@ -132,7 +132,7 @@ module.exports = {
132132
controller: 'ChallengeTimelineTemplateController',
133133
method: 'createChallengeTimelineTemplate',
134134
auth: 'jwt',
135-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
135+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
136136
scopes: [CREATE, ALL]
137137
}
138138
},
@@ -148,14 +148,14 @@ module.exports = {
148148
controller: 'ChallengeTimelineTemplateController',
149149
method: 'fullyUpdateChallengeTimelineTemplate',
150150
auth: 'jwt',
151-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
151+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
152152
scopes: [UPDATE, ALL]
153153
},
154154
delete: {
155155
controller: 'ChallengeTimelineTemplateController',
156156
method: 'deleteChallengeTimelineTemplate',
157157
auth: 'jwt',
158-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
158+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
159159
scopes: [DELETE, ALL]
160160
}
161161
},
@@ -187,7 +187,7 @@ module.exports = {
187187
controller: 'ChallengePhaseController',
188188
method: 'getPhase',
189189
auth: 'jwt',
190-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
190+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
191191
scopes: [READ, ALL]
192192
},
193193
put: {
@@ -231,7 +231,7 @@ module.exports = {
231231
controller: 'TimelineTemplateController',
232232
method: 'getTimelineTemplate',
233233
auth: 'jwt',
234-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
234+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
235235
scopes: [READ, ALL]
236236
},
237237
put: {
@@ -261,7 +261,7 @@ module.exports = {
261261
controller: 'AttachmentController',
262262
method: 'uploadAttachment',
263263
auth: 'jwt',
264-
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot],
264+
access: [constants.UserRoles.Admin, constants.UserRoles.Copilot, constants.UserRoles.Manager],
265265
scopes: [CREATE, ALL]
266266
}
267267
},

src/scripts/seed/ChallengeType.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
"description": "A piece of work assigned to one person",
2222
"isActive": true,
2323
"isTask": true,
24-
"abbreviation": "T"
24+
"abbreviation": "TSK"
2525
}
2626
]

src/services/ChallengeService.js

+63-35
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ async function searchChallenges (currentUser, criteria) {
154154
_.forIn(_.omit(criteria, ['types', 'tracks', 'typeIds', 'trackIds', 'type', 'name', 'trackId', 'typeId', 'description', 'page', 'perPage', 'tag',
155155
'group', 'groups', 'memberId', 'ids', 'createdDateStart', 'createdDateEnd', 'updatedDateStart', 'updatedDateEnd', 'startDateStart', 'startDateEnd', 'endDateStart', 'endDateEnd',
156156
'tags', 'registrationStartDateStart', 'registrationStartDateEnd', 'currentPhaseName', 'submissionStartDateStart', 'submissionStartDateEnd',
157-
'registrationEndDateStart', 'registrationEndDateEnd', 'submissionEndDateStart', 'submissionEndDateEnd',
157+
'registrationEndDateStart', 'registrationEndDateEnd', 'submissionEndDateStart', 'submissionEndDateEnd', 'includeAllEvents', 'events',
158158
'forumId', 'track', 'reviewType', 'confidentialityType', 'directProjectId', 'sortBy', 'sortOrder', 'isLightweight', 'isTask', 'taskIsAssigned', 'taskMemberId']), (value, key) => {
159159
if (!_.isUndefined(value)) {
160160
const filter = { match_phrase: {} }
@@ -251,7 +251,10 @@ async function searchChallenges (currentUser, criteria) {
251251
if (criteria.endDateEnd) {
252252
boolQuery.push({ range: { endDate: { lte: criteria.endDateEnd } } })
253253
}
254-
const sortByProp = criteria.sortBy ? criteria.sortBy : 'created'
254+
255+
let sortByProp = criteria.sortBy ? criteria.sortBy : 'created'
256+
// If property to sort is text, then use its sub-field 'keyword' for sorting
257+
sortByProp = _.includes(constants.challengeTextSortField, sortByProp) ? sortByProp + '.keyword' : sortByProp
255258
const sortOrderProp = criteria.sortOrder ? criteria.sortOrder : 'desc'
256259

257260
const mustQuery = []
@@ -271,6 +274,18 @@ async function searchChallenges (currentUser, criteria) {
271274
}
272275
}
273276

277+
if (criteria.events) {
278+
if (criteria.includeAllEvents) {
279+
for (const e of criteria.events) {
280+
boolQuery.push({ match_phrase: { 'events.key': e } })
281+
}
282+
} else {
283+
for (const e of criteria.events) {
284+
shouldQuery.push({ match: { 'events.key': e } })
285+
}
286+
}
287+
}
288+
274289
const mustNotQuery = []
275290

276291
let groupsToFilter = []
@@ -339,6 +354,29 @@ async function searchChallenges (currentUser, criteria) {
339354
}
340355
}
341356

357+
const accessQuery = []
358+
let memberChallengeIds
359+
360+
// FIXME: This is wrong!
361+
// if (!_.isUndefined(currentUser) && currentUser.handle) {
362+
// accessQuery.push({ match_phrase: { createdBy: currentUser.handle } })
363+
// }
364+
365+
if (criteria.memberId) {
366+
// logger.error(`memberId ${criteria.memberId}`)
367+
memberChallengeIds = await helper.listChallengesByMember(criteria.memberId)
368+
// logger.error(`response ${JSON.stringify(ids)}`)
369+
accessQuery.push({ terms: { _id: memberChallengeIds } })
370+
}
371+
372+
if (accessQuery.length > 0) {
373+
mustQuery.push({
374+
bool: {
375+
should: accessQuery
376+
}
377+
})
378+
}
379+
342380
// FIXME: Tech Debt
343381
let excludeTasks = true
344382
// if you're an admin or m2m, security rules wont be applied
@@ -369,6 +407,8 @@ async function searchChallenges (currentUser, criteria) {
369407
mustQuery.push({
370408
bool: {
371409
should: [
410+
...(_.get(memberChallengeIds, 'length', 0) > 0 ? [{ terms: { _id: memberChallengeIds } }] : []),
411+
{ bool: { must_not: { exists: { field: 'task.isTask' } } } },
372412
{ match_phrase: { 'task.isTask': false } },
373413
{
374414
bool: {
@@ -396,28 +436,6 @@ async function searchChallenges (currentUser, criteria) {
396436
})
397437
}
398438

399-
const accessQuery = []
400-
401-
// FIXME: This is wrong!
402-
// if (!_.isUndefined(currentUser) && currentUser.handle) {
403-
// accessQuery.push({ match_phrase: { createdBy: currentUser.handle } })
404-
// }
405-
406-
if (criteria.memberId) {
407-
// logger.error(`memberId ${criteria.memberId}`)
408-
const ids = await helper.listChallengesByMember(criteria.memberId)
409-
// logger.error(`response ${JSON.stringify(ids)}`)
410-
accessQuery.push({ terms: { _id: ids } })
411-
}
412-
413-
if (accessQuery.length > 0) {
414-
mustQuery.push({
415-
bool: {
416-
should: accessQuery
417-
}
418-
})
419-
}
420-
421439
if (boolQuery.length > 0) {
422440
mustQuery.push({
423441
bool: {
@@ -465,7 +483,7 @@ async function searchChallenges (currentUser, criteria) {
465483
docs = await esClient.search(esQuery)
466484
} catch (e) {
467485
// Catch error when the ES is fresh and has no data
468-
// logger.error(`Query Error from ES ${JSON.stringify(e)}`)
486+
logger.error(`Query Error from ES ${JSON.stringify(e)}`)
469487
docs = {
470488
hits: {
471489
total: 0,
@@ -573,7 +591,9 @@ searchChallenges.schema = {
573591
ids: Joi.array().items(Joi.optionalId()).unique().min(1),
574592
isTask: Joi.boolean(),
575593
taskIsAssigned: Joi.boolean(),
576-
taskMemberId: Joi.string()
594+
taskMemberId: Joi.string(),
595+
events: Joi.array().items(Joi.number()),
596+
includeAllEvents: Joi.boolean().default(true)
577597
})
578598
}
579599

@@ -718,6 +738,12 @@ async function createChallenge (currentUser, challenge, userToken) {
718738
const { track, type } = await validateChallengeData(challenge)
719739
if (_.get(type, 'isTask')) {
720740
_.set(challenge, 'task.isTask', true)
741+
if (_.isUndefined(_.get(challenge, 'task.isAssigned'))) {
742+
_.set(challenge, 'task.isAssigned', false)
743+
}
744+
if (_.isUndefined(_.get(challenge, 'task.memberId'))) {
745+
_.set(challenge, 'task.memberId', null)
746+
}
721747
}
722748
if (challenge.phases && challenge.phases.length > 0) {
723749
await helper.validatePhases(challenge.phases)
@@ -921,25 +947,27 @@ async function getChallenge (currentUser, id) {
921947
// }
922948
// delete challenge.typeId
923949

924-
// Check if challenge is task and apply security rules
925-
if (_.get(challenge, 'task.isTask', false) && _.get(challenge, 'task.isAssigned', false)) {
926-
if (!currentUser || (!currentUser.isMachine && !helper.hasAdminRole(currentUser) && _.toString(currentUser.userId) !== _.toString(_.get(challenge, 'task.memberId')))) {
927-
throw new errors.ForbiddenError(`You don't have access to view this challenge`)
928-
}
929-
}
930-
950+
let memberChallengeIds
931951
// Remove privateDescription for unregistered users
932952
if (currentUser) {
933953
if (!currentUser.isMachine) {
934-
const ids = await helper.listChallengesByMember(currentUser.userId)
935-
if (!_.includes(ids, challenge.id)) {
954+
memberChallengeIds = await helper.listChallengesByMember(currentUser.userId)
955+
if (!_.includes(memberChallengeIds, challenge.id)) {
936956
_.unset(challenge, 'privateDescription')
937957
}
938958
}
939959
} else {
940960
_.unset(challenge, 'privateDescription')
941961
}
942962

963+
// Check if challenge is task and apply security rules
964+
if (_.get(challenge, 'task.isTask', false) && _.get(challenge, 'task.isAssigned', false)) {
965+
const canAccesChallenge = _.isUndefined(currentUser) ? false : _.includes((memberChallengeIds || []), challenge.id) || currentUser.isMachine || helper.hasAdminRole(currentUser)
966+
if (!canAccesChallenge) {
967+
throw new errors.ForbiddenError(`You don't have access to view this challenge`)
968+
}
969+
}
970+
943971
if (challenge.phases && challenge.phases.length > 0) {
944972
await getPhasesAndPopulate(challenge)
945973
}

0 commit comments

Comments
 (0)