Skip to content

Commit ddf213e

Browse files
committedOct 9, 2020
Merge branch 'develop'
2 parents 55dd751 + 2c7d92d commit ddf213e

9 files changed

+754
-920
lines changed
 

‎app-constants.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ const EVENT_ORIGINATOR = 'topcoder-challenges-api'
4747

4848
const EVENT_MIME_TYPE = 'application/json'
4949

50+
const DiscussionTypes = {
51+
Challenge: 'challenge'
52+
}
53+
5054
// using a testing topc, should be changed to use real topics in comments when they are created
5155
const Topics = {
5256
ChallengeCreated: 'challenge.notification.create',
@@ -85,5 +89,6 @@ module.exports = {
8589
EVENT_MIME_TYPE,
8690
Topics,
8791
challengeTracks,
88-
challengeTextSortField
92+
challengeTextSortField,
93+
DiscussionTypes
8994
}

‎config/default.js

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ module.exports = {
4747
FILE_UPLOAD_SIZE_LIMIT: process.env.FILE_UPLOAD_SIZE_LIMIT
4848
? Number(process.env.FILE_UPLOAD_SIZE_LIMIT) : 50 * 1024 * 1024, // 50M
4949
RESOURCES_API_URL: process.env.RESOURCES_API_URL || 'http://localhost:4000/v5/resources',
50+
// TODO: change this to localhost
51+
RESOURCE_ROLES_API_URL: process.env.RESOURCE_ROLES_API_URL || 'http://api.topcoder-dev.com/v5/resource-roles',
5052
GROUPS_API_URL: process.env.GROUPS_API_URL || 'http://localhost:4000/v5/groups',
5153
PROJECTS_API_URL: process.env.PROJECTS_API_URL || 'http://localhost:4000/v5/projects',
5254
TERMS_API_URL: process.env.TERMS_API_URL || 'http://localhost:4000/v5/terms',

‎docs/swagger.yaml

+30
Original file line numberDiff line numberDiff line change
@@ -2219,6 +2219,28 @@ definitions:
22192219
- $ref: '#/definitions/EventData'
22202220
required:
22212221
- id
2222+
Discussion:
2223+
type: object
2224+
properties:
2225+
id:
2226+
type: string
2227+
format: UUID
2228+
name:
2229+
type: string
2230+
type:
2231+
type: string
2232+
enum:
2233+
- challenge
2234+
provider:
2235+
type: string
2236+
url:
2237+
type: string
2238+
description: Only M2M tokens can modify this
2239+
options:
2240+
type: array
2241+
description: Only M2M tokens can modify this
2242+
items:
2243+
type: object
22222244
TimelineTemplate:
22232245
type: object
22242246
allOf:
@@ -2342,6 +2364,10 @@ definitions:
23422364
type: string
23432365
value:
23442366
type: number
2367+
discussions:
2368+
type: array
2369+
items:
2370+
$ref: '#/definitions/Discussion'
23452371
tags:
23462372
type: array
23472373
items:
@@ -2655,6 +2681,10 @@ definitions:
26552681
format: UUID
26562682
duration:
26572683
type: number
2684+
discussions:
2685+
type: array
2686+
items:
2687+
$ref: '#/definitions/Discussion'
26582688
prizeSets:
26592689
type: array
26602690
items:

‎docs/topcoder-challenge-api.postman_collection.json

+485-867
Large diffs are not rendered by default.

‎package-lock.json

+15-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"topcoder-bus-api-wrapper": "topcoder-platform/tc-bus-api-wrapper.git",
5656
"uuid": "^3.3.2",
5757
"winston": "^3.1.0",
58-
"xss": "^1.0.6",
58+
"xss": "^1.0.8",
5959
"yamljs": "^0.3.0"
6060
},
6161
"standard": {

‎src/common/helper.js

+62-7
Original file line numberDiff line numberDiff line change
@@ -392,11 +392,46 @@ async function getM2MToken () {
392392
*/
393393
async function getChallengeResources (challengeId) {
394394
const token = await getM2MToken()
395-
const url = `${config.RESOURCES_API_URL}?challengeId=${challengeId}`
396-
const res = await axios.get(url, { headers: { Authorization: `Bearer ${token}` } })
395+
const perPage = 100
396+
let page = 1
397+
let result = []
398+
while (true) {
399+
const url = `${config.RESOURCES_API_URL}?challengeId=${challengeId}&perPage=${perPage}&page=${page}`
400+
const res = await axios.get(url, { headers: { Authorization: `Bearer ${token}` } })
401+
if (!res.data || res.data.length === 0) {
402+
break
403+
}
404+
result = result.concat(res.data)
405+
page += 1
406+
if (res.headers['x-total-pages'] && page > Number(res.headers['x-total-pages'])) {
407+
break
408+
}
409+
}
410+
return result
411+
}
412+
413+
/**
414+
* Get resource roles
415+
* @returns {Promise<Array>} the challenge resources
416+
*/
417+
async function getResourceRoles () {
418+
const token = await getM2MToken()
419+
const res = await axios.get(config.RESOURCE_ROLES_API_URL, { headers: { Authorization: `Bearer ${token}` } })
397420
return res.data || []
398421
}
399422

423+
/**
424+
* Check if a user has full access on a challenge
425+
* @param {String} challengeId the challenge UUID
426+
* @param {String} userId the user ID
427+
*/
428+
async function userHasFullAccess (challengeId, userId) {
429+
const resourceRoles = await getResourceRoles()
430+
const rolesWithFullAccess = _.map(_.filter(resourceRoles, r => r.fullAccess), 'id')
431+
const challengeResources = await getChallengeResources(challengeId)
432+
return _.filter(challengeResources, r => _.toString(r.memberId) === _.toString(userId) && _.includes(rolesWithFullAccess, r.roleId)).length > 0
433+
}
434+
400435
/**
401436
* Get all user groups
402437
* @param {String} userId the user id
@@ -647,8 +682,17 @@ async function validateESRefreshMethod (method) {
647682
async function getProjectDefaultTerms (projectId) {
648683
const token = await getM2MToken()
649684
const projectUrl = `${config.PROJECTS_API_URL}/${projectId}`
650-
const res = await axios.get(projectUrl, { headers: { Authorization: `Bearer ${token}` } })
651-
return res.data.terms || []
685+
try {
686+
const res = await axios.get(projectUrl, { headers: { Authorization: `Bearer ${token}` } })
687+
return res.data.terms || []
688+
} catch (err) {
689+
if (_.get(err, 'response.status') === HttpStatus.NOT_FOUND) {
690+
throw new errors.BadRequestError(`Project with id: ${projectId} doesn't exist`)
691+
} else {
692+
// re-throw other error
693+
throw err
694+
}
695+
}
652696
}
653697

654698
/**
@@ -660,8 +704,17 @@ async function getProjectDefaultTerms (projectId) {
660704
async function getProjectBillingAccount (projectId) {
661705
const token = await getM2MToken()
662706
const projectUrl = `${config.V3_PROJECTS_API_URL}/${projectId}`
663-
const res = await axios.get(projectUrl, { headers: { Authorization: `Bearer ${token}` } })
664-
return _.get(res, 'data.result.content.billingAccountIds[0]', null)
707+
try {
708+
const res = await axios.get(projectUrl, { headers: { Authorization: `Bearer ${token}` } })
709+
return _.get(res, 'data.result.content.billingAccountIds[0]', null)
710+
} catch (err) {
711+
if (_.get(err, 'response.status') === HttpStatus.NOT_FOUND) {
712+
throw new errors.BadRequestError(`Project with id: ${projectId} doesn't exist`)
713+
} else {
714+
// re-throw other error
715+
throw err
716+
}
717+
}
665718
}
666719

667720
/**
@@ -723,5 +776,7 @@ module.exports = {
723776
getProjectBillingAccount,
724777
expandWithSubGroups,
725778
getCompleteUserGroupTreeIds,
726-
expandWithParentGroups
779+
expandWithParentGroups,
780+
getResourceRoles,
781+
userHasFullAccess
727782
}

‎src/models/Challenge.js

+4
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ const schema = new Schema({
108108
type: Array,
109109
required: false
110110
},
111+
discussions: {
112+
type: [Object],
113+
required: false
114+
},
111115
created: {
112116
type: Date,
113117
required: true

‎src/services/ChallengeService.js

+149-38
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const _ = require('lodash')
66
const Joi = require('joi')
77
const uuid = require('uuid/v4')
88
const config = require('config')
9+
const xss = require('xss')
910
const helper = require('../common/helper')
1011
const logger = require('../common/logger')
1112
const errors = require('../common/errors')
@@ -109,6 +110,15 @@ async function searchChallenges (currentUser, criteria) {
109110
const page = criteria.page || 1
110111
const perPage = criteria.perPage || 20
111112
const boolQuery = []
113+
const matchPhraseKeys = [
114+
'id',
115+
'timelineTemplateId',
116+
'projectId',
117+
'legacyId',
118+
'status',
119+
'createdBy',
120+
'updatedBy'
121+
]
112122

113123
const includedTrackIds = _.isArray(criteria.trackIds) ? criteria.trackIds : []
114124

@@ -151,18 +161,24 @@ async function searchChallenges (currentUser, criteria) {
151161
includedTrackIds.push(criteria.trackId)
152162
}
153163

154-
_.forIn(_.omit(criteria, ['types', 'tracks', 'typeIds', 'trackIds', 'type', 'name', 'trackId', 'typeId', 'description', 'page', 'perPage', 'tag',
155-
'group', 'groups', 'memberId', 'ids', 'createdDateStart', 'createdDateEnd', 'updatedDateStart', 'updatedDateEnd', 'startDateStart', 'startDateEnd', 'endDateStart', 'endDateEnd',
156-
'tags', 'registrationStartDateStart', 'registrationStartDateEnd', 'currentPhaseName', 'submissionStartDateStart', 'submissionStartDateEnd',
157-
'registrationEndDateStart', 'registrationEndDateEnd', 'submissionEndDateStart', 'submissionEndDateEnd', 'includeAllEvents', 'events',
158-
'forumId', 'track', 'reviewType', 'confidentialityType', 'directProjectId', 'sortBy', 'sortOrder', 'isLightweight', 'isTask', 'taskIsAssigned', 'taskMemberId']), (value, key) => {
164+
_.forIn(_.pick(criteria, matchPhraseKeys), (value, key) => {
159165
if (!_.isUndefined(value)) {
160166
const filter = { match_phrase: {} }
161167
filter.match_phrase[key] = value
162168
boolQuery.push(filter)
163169
}
164170
})
165171

172+
_.forEach(_.keys(criteria), (key) => {
173+
if (_.toString(key).indexOf('meta.') > -1) {
174+
// Parse and use metadata key
175+
if (!_.isUndefined(criteria[key])) {
176+
const metaKey = key.split('meta.')[1]
177+
boolQuery.push({ match_phrase: { [`metadata.${metaKey}`]: criteria[key] } })
178+
}
179+
}
180+
})
181+
166182
if (includedTypeIds.length > 0) {
167183
boolQuery.push({
168184
bool: {
@@ -180,10 +196,10 @@ async function searchChallenges (currentUser, criteria) {
180196
}
181197

182198
if (criteria.name) {
183-
boolQuery.push({ match: { name: `.*${criteria.name}.*` } })
199+
boolQuery.push({ wildcard: { name: `*${_.toLower(criteria.name)}*` } })
184200
}
185201
if (criteria.description) {
186-
boolQuery.push({ match: { description: `.*${criteria.name}.*` } })
202+
boolQuery.push({ wildcard: { description: `*${_.toLower(criteria.description)}*` } })
187203
}
188204
if (criteria.forumId) {
189205
boolQuery.push({ match_phrase: { 'legacy.forumId': criteria.forumId } })
@@ -259,31 +275,23 @@ async function searchChallenges (currentUser, criteria) {
259275

260276
const mustQuery = []
261277

262-
const shouldQuery = []
278+
const groupsQuery = []
263279

264280
// logger.debug(`Tags: ${criteria.tags}`)
265281
if (criteria.tags) {
266-
if (criteria.includeAllTags) {
267-
for (const tag of criteria.tags) {
268-
boolQuery.push({ match_phrase: { tags: tag } })
269-
}
270-
} else {
271-
for (const tag of criteria.tags) {
272-
shouldQuery.push({ match: { tags: tag } })
282+
boolQuery.push({
283+
bool: {
284+
[criteria.includeAllTags ? 'must' : 'should']: _.map(criteria.tags, t => ({ match_phrase: { tags: t } }))
273285
}
274-
}
286+
})
275287
}
276288

277289
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 } })
290+
boolQuery.push({
291+
bool: {
292+
[criteria.includeAllEvents ? 'must' : 'should']: _.map(criteria.events, e => ({ match_phrase: { 'events.key': e } }))
285293
}
286-
}
294+
})
287295
}
288296

289297
const mustNotQuery = []
@@ -337,21 +345,23 @@ async function searchChallenges (currentUser, criteria) {
337345
} else if (!currentUser.isMachine && !helper.hasAdminRole(currentUser)) {
338346
// If the user is not M2M and is not an admin, return public + challenges from groups the user can access
339347
_.each(accessibleGroups, (g) => {
340-
shouldQuery.push({ match_phrase: { groups: g } })
348+
groupsQuery.push({ match_phrase: { groups: g } })
341349
})
342350
// include public challenges
343-
shouldQuery.push({ bool: { must_not: { exists: { field: 'groups' } } } })
351+
groupsQuery.push({ bool: { must_not: { exists: { field: 'groups' } } } })
344352
}
345353
} else {
346354
_.each(groupsToFilter, (g) => {
347-
shouldQuery.push({ match_phrase: { groups: g } })
355+
groupsQuery.push({ match_phrase: { groups: g } })
348356
})
349357
}
350358

351359
if (criteria.ids) {
352-
for (const id of criteria.ids) {
353-
shouldQuery.push({ match_phrase: { _id: id } })
354-
}
360+
boolQuery.push({
361+
bool: {
362+
should: _.map(criteria.ids, id => ({ match_phrase: { _id: id } }))
363+
}
364+
})
355365
}
356366

357367
const accessQuery = []
@@ -367,6 +377,8 @@ async function searchChallenges (currentUser, criteria) {
367377
memberChallengeIds = await helper.listChallengesByMember(criteria.memberId)
368378
// logger.error(`response ${JSON.stringify(ids)}`)
369379
accessQuery.push({ terms: { _id: memberChallengeIds } })
380+
} else if (currentUser && !helper.hasAdminRole(currentUser) && !_.get(currentUser, 'isMachine', false)) {
381+
memberChallengeIds = await helper.listChallengesByMember(currentUser.userId)
370382
}
371383

372384
if (accessQuery.length > 0) {
@@ -407,7 +419,7 @@ async function searchChallenges (currentUser, criteria) {
407419
mustQuery.push({
408420
bool: {
409421
should: [
410-
...(_.get(memberChallengeIds, 'length', 0) > 0 ? [{ terms: { _id: memberChallengeIds } }] : []),
422+
...(_.get(memberChallengeIds, 'length', 0) > 0 ? [{ bool: { should: [ { terms: { _id: memberChallengeIds } } ] } }] : []),
411423
{ bool: { must_not: { exists: { field: 'task.isTask' } } } },
412424
{ match_phrase: { 'task.isTask': false } },
413425
{
@@ -428,10 +440,10 @@ async function searchChallenges (currentUser, criteria) {
428440
})
429441
}
430442

431-
if (shouldQuery.length > 0) {
443+
if (groupsQuery.length > 0) {
432444
mustQuery.push({
433445
bool: {
434-
should: shouldQuery
446+
should: groupsQuery
435447
}
436448
})
437449
}
@@ -594,7 +606,7 @@ searchChallenges.schema = {
594606
taskMemberId: Joi.string(),
595607
events: Joi.array().items(Joi.number()),
596608
includeAllEvents: Joi.boolean().default(true)
597-
})
609+
}).unknown(true)
598610
}
599611

600612
/**
@@ -731,6 +743,8 @@ async function populatePhases (phases, startDate, timelineTemplateId) {
731743
* @returns {Object} the created challenge
732744
*/
733745
async function createChallenge (currentUser, challenge, userToken) {
746+
challenge.name = xss(challenge.name)
747+
challenge.description = xss(challenge.description)
734748
if (challenge.status === constants.challengeStatuses.Active) {
735749
throw new errors.BadRequestError('You cannot create an Active challenge. Please create a Draft challenge and then change the status to Active.')
736750
}
@@ -743,6 +757,13 @@ async function createChallenge (currentUser, challenge, userToken) {
743757
}
744758
if (_.isUndefined(_.get(challenge, 'task.memberId'))) {
745759
_.set(challenge, 'task.memberId', null)
760+
} else {
761+
throw new errors.BadRequestError(`Cannot assign a member before the challenge gets created.`)
762+
}
763+
}
764+
if (challenge.discussions && challenge.discussions.length > 0) {
765+
for (let i = 0; i < challenge.discussions.length; i += 1) {
766+
challenge.discussions[i].id = uuid()
746767
}
747768
}
748769
if (challenge.phases && challenge.phases.length > 0) {
@@ -782,6 +803,11 @@ async function createChallenge (currentUser, challenge, userToken) {
782803
// this will need to be updated to associate project terms with a roleId
783804
challenge.terms = await helper.validateChallengeTerms(challenge.terms || [])
784805

806+
// default the descriptionFormat
807+
if (!challenge.descriptionFormat) {
808+
challenge.descriptionFormat = 'markdown'
809+
}
810+
785811
if (challenge.phases && challenge.phases.length > 0) {
786812
challenge.endDate = helper.calculateChallengeEndDate(challenge)
787813
}
@@ -866,6 +892,13 @@ createChallenge.schema = {
866892
name: Joi.string(),
867893
key: Joi.string()
868894
})),
895+
discussions: Joi.array().items(Joi.object().keys({
896+
name: Joi.string().required(),
897+
type: Joi.string().required().valid(_.values(constants.DiscussionTypes)),
898+
provider: Joi.string().required(),
899+
url: Joi.string(),
900+
options: Joi.array().items(Joi.object())
901+
})),
869902
prizeSets: Joi.array().items(Joi.object().keys({
870903
type: Joi.string().valid(_.values(constants.prizeSetTypes)).required(),
871904
description: Joi.string(),
@@ -1122,6 +1155,11 @@ async function update (currentUser, challengeId, data, userToken, isFull) {
11221155
throw new errors.BadRequestError('You cannot activate the challenge as it has not been created on legacy yet. Please try again later or contact support.')
11231156
}
11241157
billingAccountId = await helper.getProjectBillingAccount(_.get(challenge, 'legacy.directProjectId'))
1158+
// if activating a challenge, the challenge must have a billing account id
1159+
if ((!billingAccountId || billingAccountId === null) &&
1160+
challenge.status === constants.challengeStatuses.Draft) {
1161+
throw new errors.BadRequestError('Cannot Activate this project, it has no active billing accounts.')
1162+
}
11251163
}
11261164
if (data.status === constants.challengeStatuses.Completed) {
11271165
if (challenge.status !== constants.challengeStatuses.Active) {
@@ -1159,8 +1197,30 @@ async function update (currentUser, challengeId, data, userToken, isFull) {
11591197
newAttachments = await helper.getByIds('Attachment', data.attachmentIds || [])
11601198
}
11611199

1162-
if (!currentUser.isMachine && !helper.hasAdminRole(currentUser) && challenge.createdBy.toLowerCase() !== currentUser.handle.toLowerCase()) {
1163-
throw new errors.ForbiddenError(`Only M2M, admin or challenge's copilot can perform modification.`)
1200+
const userHasFullAccess = await helper.userHasFullAccess(challengeId, currentUser.userId)
1201+
if (!currentUser.isMachine && !helper.hasAdminRole(currentUser) && challenge.createdBy.toLowerCase() !== currentUser.handle.toLowerCase() && !userHasFullAccess) {
1202+
throw new errors.ForbiddenError(`Only M2M, admin, challenge's copilot or users with full access can perform modification.`)
1203+
}
1204+
1205+
// Only M2M can update url and options of discussions
1206+
if (data.discussions && data.discussions.length > 0) {
1207+
for (let i = 0; i < data.discussions.length; i += 1) {
1208+
if (_.isUndefined(data.discussions[i].id)) {
1209+
data.discussions[i].id = uuid()
1210+
if (!currentUser.isMachine) {
1211+
_.unset(data.discussions, 'url')
1212+
_.unset(data.discussions, 'options')
1213+
}
1214+
} else if (!currentUser.isMachine) {
1215+
const existingDiscussion = _.find(_.get(challenge, 'discussions', []), d => d.id === data.discussions[i].id)
1216+
if (existingDiscussion) {
1217+
_.assign(data.discussions[i], _.pick(existingDiscussion, ['url', 'options']))
1218+
} else {
1219+
_.unset(data.discussions, 'url')
1220+
_.unset(data.discussions, 'options')
1221+
}
1222+
}
1223+
}
11641224
}
11651225

11661226
// Validate the challenge terms
@@ -1211,7 +1271,15 @@ async function update (currentUser, challengeId, data, userToken, isFull) {
12111271
}
12121272

12131273
if (data.phases || data.startDate) {
1214-
const newPhases = data.phases || challenge.phases
1274+
if (data.phases && data.phases.length > 0) {
1275+
for (let i = 0; i < challenge.phases.length; i += 1) {
1276+
const updatedPhaseInfo = _.find(data.phases, p => p.phaseId === challenge.phases[i].phaseId)
1277+
if (updatedPhaseInfo) {
1278+
_.extend(challenge.phases[i], updatedPhaseInfo)
1279+
}
1280+
}
1281+
}
1282+
const newPhases = challenge.phases
12151283
const newStartDate = data.startDate || challenge.startDate
12161284

12171285
await helper.validatePhases(newPhases)
@@ -1419,6 +1487,27 @@ async function update (currentUser, challengeId, data, userToken, isFull) {
14191487
data.winners = null
14201488
}
14211489

1490+
const { track, type } = await validateChallengeData(_.pick(challenge, ['trackId', 'typeId']))
1491+
1492+
// Only m2m tokens are allowed to modify the `task.*` information on a challenge
1493+
if (!_.isUndefined(_.get(data, 'task')) && !currentUser.isMachine) {
1494+
if (!_.isUndefined(_.get(challenge, 'task'))) {
1495+
data.task = challenge.task
1496+
} else {
1497+
delete data.task
1498+
}
1499+
}
1500+
1501+
if (_.get(type, 'isTask')) {
1502+
if (!_.isEmpty(_.get(data, 'task.memberId'))) {
1503+
const challengeResources = await helper.getChallengeResources(challengeId)
1504+
const registrants = _.filter(challengeResources, r => r.roleId === config.SUBMITTER_ROLE_ID)
1505+
if (!_.find(registrants, r => _.toString(r.memberId) === _.toString(_.get(data, 'task.memberId')))) {
1506+
throw new errors.BadRequestError(`Member ${_.get(data, 'task.memberId')} is not a submitter resource of challenge ${challengeId}`)
1507+
}
1508+
}
1509+
}
1510+
14221511
logger.debug(`Challenge.update id: ${challengeId} Details: ${JSON.stringify(updateDetails)}`)
14231512
await models.Challenge.update({ id: challengeId }, updateDetails)
14241513

@@ -1459,7 +1548,6 @@ async function update (currentUser, challengeId, data, userToken, isFull) {
14591548
}
14601549

14611550
// Populate challenge.track and challenge.type based on the track/type IDs
1462-
const { track, type } = await validateChallengeData(_.pick(challenge, ['trackId', 'typeId']))
14631551

14641552
if (track) {
14651553
challenge.track = track.name
@@ -1515,6 +1603,12 @@ function sanitizeChallenge (challenge) {
15151603
'attachmentIds',
15161604
'groups'
15171605
])
1606+
if (!_.isUndefined(sanitized.name)) {
1607+
sanitized.name = xss(sanitized.name)
1608+
}
1609+
if (!_.isUndefined(sanitized.description)) {
1610+
sanitized.description = xss(sanitized.description)
1611+
}
15181612
if (challenge.legacy) {
15191613
sanitized.legacy = _.pick(challenge.legacy, [
15201614
'track',
@@ -1546,6 +1640,9 @@ function sanitizeChallenge (challenge) {
15461640
if (challenge.winners) {
15471641
sanitized.winners = _.map(challenge.winners, winner => _.pick(winner, ['userId', 'handle', 'placement']))
15481642
}
1643+
if (challenge.discussions) {
1644+
sanitized.discussions = _.map(challenge.discussions, discussion => _.pick(discussion, ['id', 'provider', 'name', 'type', 'url', 'options']))
1645+
}
15491646
if (challenge.terms) {
15501647
sanitized.terms = _.map(challenge.terms, term => _.pick(term, ['id', 'roleId']))
15511648
}
@@ -1614,6 +1711,13 @@ fullyUpdateChallenge.schema = {
16141711
name: Joi.string(),
16151712
key: Joi.string()
16161713
}).unknown(true)),
1714+
discussions: Joi.array().items(Joi.object().keys({
1715+
name: Joi.string().required(),
1716+
type: Joi.string().required().valid(_.values(constants.DiscussionTypes)),
1717+
provider: Joi.string().required(),
1718+
url: Joi.string(),
1719+
options: Joi.array().items(Joi.object())
1720+
})),
16171721
tags: Joi.array().items(Joi.string().required()), // tag names
16181722
projectId: Joi.number().integer().positive().required(),
16191723
legacyId: Joi.number().integer().positive(),
@@ -1686,6 +1790,13 @@ partiallyUpdateChallenge.schema = {
16861790
name: Joi.string(),
16871791
key: Joi.string()
16881792
}).unknown(true)),
1793+
discussions: Joi.array().items(Joi.object().keys({
1794+
name: Joi.string().required(),
1795+
type: Joi.string().required().valid(_.values(constants.DiscussionTypes)),
1796+
provider: Joi.string().required(),
1797+
url: Joi.string(),
1798+
options: Joi.array().items(Joi.object())
1799+
})),
16891800
startDate: Joi.date(),
16901801
prizeSets: Joi.array().items(Joi.object().keys({
16911802
type: Joi.string().valid(_.values(constants.prizeSetTypes)).required(),

0 commit comments

Comments
 (0)
Please sign in to comment.