Skip to content

Commit c4db4d7

Browse files
authored
Merge pull request #133 from topcoder-platform/PM-5223-4
PM-5223: route QA challenge ratings to QA
2 parents c394963 + b9fdf99 commit c4db4d7

9 files changed

Lines changed: 193 additions & 86 deletions

src/common/prismaHelper.js

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ const marathonRankFields = [
4949
'minimumRating', 'maximumRating', 'countryRank', 'schoolRank', 'defaultLanguage'
5050
]
5151

52+
const groupedSubTrackStatsTrackNames = ['DEVELOP', 'DESIGN', 'QA']
53+
const supportedUnifiedStatsTrackNames = ['DEVELOP', 'DESIGN', 'DATA_SCIENCE', 'QA', 'COPILOT']
54+
const supportedUnifiedHistoryTrackNames = ['DEVELOP', 'DESIGN', 'DATA_SCIENCE', 'QA']
55+
5256
const auditFields = [
5357
'createdAt', 'createdBy', 'updatedAt', 'updatedBy'
5458
]
@@ -331,8 +335,10 @@ function applyMaxRatingRankFallback (item) {
331335
return
332336
}
333337

334-
if (trackName === 'DEVELOP' && item.DEVELOP && _.isArray(item.DEVELOP.subTracks)) {
335-
const statsItem = _.find(item.DEVELOP.subTracks, subTrack =>
338+
if ((trackName === 'DEVELOP' || trackName === 'QA') &&
339+
item[trackName] &&
340+
_.isArray(item[trackName].subTracks)) {
341+
const statsItem = _.find(item[trackName].subTracks, subTrack =>
336342
getUnifiedTypeName(subTrack.id || subTrack.name) === typeName
337343
)
338344
if (statsItem) {
@@ -616,7 +622,7 @@ function buildUnifiedStatsResponse (member, statsData, fields) {
616622
resolvedTrackName: getUnifiedTrackName(row.trackName || row.trackId),
617623
resolvedTypeName: getUnifiedTypeName(row.typeName || row.typeId)
618624
}))
619-
.filter(row => _.includes(['DEVELOP', 'DESIGN', 'DATA_SCIENCE', 'COPILOT'], row.resolvedTrackName))
625+
.filter(row => _.includes(supportedUnifiedStatsTrackNames, row.resolvedTrackName))
620626
.value()
621627
const first = _.head(validRows) || {}
622628
const item = {
@@ -635,18 +641,18 @@ function buildUnifiedStatsResponse (member, statsData, fields) {
635641
_.forEach(validRows, (row) => {
636642
const trackName = row.resolvedTrackName
637643
const typeName = row.resolvedTypeName
638-
if (trackName === 'DEVELOP') {
644+
if (trackName === 'DEVELOP' || trackName === 'QA') {
639645
const challengeCount = toNumber(row.challenges)
640-
if (!item.DEVELOP) {
641-
item.DEVELOP = {
646+
if (!item[trackName]) {
647+
item[trackName] = {
642648
challenges: 0,
643649
wins: 0,
644650
mostRecentSubmission: null,
645651
mostRecentEventDate: null,
646652
subTracks: []
647653
}
648654
}
649-
mergeTrackCounters(item.DEVELOP, row)
655+
mergeTrackCounters(item[trackName], row)
650656
const subTrackItem = {
651657
id: typeName,
652658
name: typeName,
@@ -683,7 +689,7 @@ function buildUnifiedStatsResponse (member, statsData, fields) {
683689
rank.minRating = row.minRating
684690
}
685691
subTrackItem.rank = rank
686-
item.DEVELOP.subTracks.push(subTrackItem)
692+
item[trackName].subTracks.push(subTrackItem)
687693
} else if (trackName === 'DESIGN') {
688694
if (!item.DESIGN) {
689695
item.DESIGN = {
@@ -806,7 +812,7 @@ function buildUnifiedStatsHistoryResponse (member, historyStats, fields) {
806812
resolvedTrackName: getUnifiedTrackName(row.trackName || row.trackId),
807813
resolvedTypeName: getUnifiedTypeName(row.typeName || row.typeId)
808814
}))
809-
.filter(row => _.includes(['DEVELOP', 'DESIGN', 'DATA_SCIENCE'], row.resolvedTrackName))
815+
.filter(row => _.includes(supportedUnifiedHistoryTrackNames, row.resolvedTrackName))
810816
.value()
811817
const first = _.head(validRows) || {}
812818
const item = {
@@ -819,8 +825,8 @@ function buildUnifiedStatsHistoryResponse (member, historyStats, fields) {
819825
const groupedByTrackType = _.groupBy(validRows, row => `${row.resolvedTrackName}::${row.resolvedTypeName}`)
820826
_.forEach(groupedByTrackType, (trackHistory, key) => {
821827
const [trackName, typeName] = key.split('::')
822-
if (trackName === 'DEVELOP' || trackName === 'DESIGN') {
823-
const historyTrackName = trackName === 'DESIGN' ? 'DESIGN' : 'DEVELOP'
828+
if (_.includes(groupedSubTrackStatsTrackNames, trackName)) {
829+
const historyTrackName = trackName
824830
if (!item[historyTrackName]) {
825831
item[historyTrackName] = { subTracks: [] }
826832
}

src/common/statsDimensionHelper.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const TRACK_NAMES = {
44
DEVELOP: 'DEVELOP',
55
DESIGN: 'DESIGN',
66
DATA_SCIENCE: 'DATA_SCIENCE',
7+
QA: 'QA',
78
COPILOT: 'COPILOT'
89
}
910

@@ -51,6 +52,10 @@ function getCanonicalTrackName (value) {
5152
return TRACK_NAMES.DATA_SCIENCE
5253
}
5354

55+
if (normalized === 'QA' || (normalized.includes('QUALITY') && normalized.includes('ASSURANCE'))) {
56+
return TRACK_NAMES.QA
57+
}
58+
5459
if (normalized.includes('DEVELOP') || normalized === 'DEV') {
5560
return TRACK_NAMES.DEVELOP
5661
}
@@ -143,6 +148,9 @@ function buildChallengeDimensionLookup (trackRows, typeRows) {
143148
addLookupEntry(trackIdsByLookup, row.abbreviation, id)
144149
addLookupEntry(trackIdsByLookup, row.legacyId, id)
145150
addLookupEntry(trackIdsByLookup, canonicalTrackName, id)
151+
if (canonicalTrackName === TRACK_NAMES.QA) {
152+
addLookupEntry(trackIdsByLookup, 'QUALITY_ASSURANCE', id)
153+
}
146154
})
147155

148156
typeRows.forEach((row) => {
@@ -173,6 +181,7 @@ function buildChallengeDimensionLookup (trackRows, typeRows) {
173181
DEVELOP: resolveTrackIdFromLookup(lookup, TRACK_NAMES.DEVELOP) || null,
174182
DESIGN: resolveTrackIdFromLookup(lookup, TRACK_NAMES.DESIGN) || null,
175183
DATA_SCIENCE: resolveTrackIdFromLookup(lookup, TRACK_NAMES.DATA_SCIENCE) || null,
184+
QA: resolveTrackIdFromLookup(lookup, TRACK_NAMES.QA) || null,
176185
COPILOT: resolveTrackIdFromLookup(lookup, TRACK_NAMES.COPILOT) || null
177186
}
178187
lookup.typeIds = {

src/scripts/recalculateMemberStats.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ function isMarathonMatchTypeId (typeId) {
366366
* Normalize challenge metadata dimensions into the public stats dimensions.
367367
* Marathon Match stats/history belong to DATA_SCIENCE even when the source
368368
* challenge row was imported with a Development track. QA Challenge rows belong
369-
* to the legacy Testing surface under DEVELOP / BUG_HUNT.
369+
* to the first-class QA / Challenge dimension.
370370
* @param {*} trackId raw ChallengeTrack id
371371
* @param {*} typeId raw ChallengeType id
372372
* @returns {Object} normalized trackId/typeId pair
@@ -386,11 +386,11 @@ function normalizeChallengeStatsDimension (trackId, typeId) {
386386
if (legacyLookupCache &&
387387
(normalizedTrackName === 'QUALITY_ASSURANCE' || normalizedTrackName === 'QA') &&
388388
typeName === TYPE_NAMES.CHALLENGE &&
389-
legacyLookupCache.trackIds.DEVELOP &&
390-
legacyLookupCache.typeIds.BUG_HUNT) {
389+
legacyLookupCache.trackIds.QA &&
390+
legacyLookupCache.typeIds.CHALLENGE) {
391391
return {
392-
trackId: legacyLookupCache.trackIds.DEVELOP,
393-
typeId: legacyLookupCache.typeIds.BUG_HUNT
392+
trackId: legacyLookupCache.trackIds.QA,
393+
typeId: legacyLookupCache.typeIds.CHALLENGE
394394
}
395395
}
396396

src/services/SearchService.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const MEMBER_AUTOCOMPLETE_FIELDS = ['userId', 'handle', 'handleLower',
4949

5050
var MEMBER_STATS_FIELDS = ['userId', 'handle', 'handleLower', 'maxRating',
5151
'numberOfChallengesWon', 'numberOfChallengesPlaced',
52-
'challenges', 'wins', 'DEVELOP', 'DESIGN', 'DATA_SCIENCE', 'COPILOT']
52+
'challenges', 'wins', 'DEVELOP', 'DESIGN', 'DATA_SCIENCE', 'QA', 'COPILOT']
5353

5454
const SKILL_LEVEL_WEIGHTS = {
5555
verified: 1.0,

src/services/StatisticsService.js

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ const DISTRIBUTION_FIELDS = ['track', 'subTrack', 'distribution', 'createdAt', '
4848
'createdBy', 'updatedBy']
4949
const DISTRIBUTION_FIELDS_NO_DATE = ['track', 'subTrack', 'distribution']
5050

51-
const HISTORY_STATS_FIELDS = ['userId', 'groupId', 'handle', 'handleLower', 'DEVELOP', 'DESIGN', 'DATA_SCIENCE',
51+
const HISTORY_STATS_FIELDS = ['userId', 'groupId', 'handle', 'handleLower', 'DEVELOP', 'DESIGN', 'DATA_SCIENCE', 'QA',
5252
'createdAt', 'updatedAt', 'createdBy', 'updatedBy']
5353

5454
const MEMBER_STATS_FIELDS = ['userId', 'groupId', 'handle', 'handleLower', 'maxRating',
55-
'challenges', 'wins', 'DEVELOP', 'DESIGN', 'DATA_SCIENCE', 'COPILOT', 'createdAt',
55+
'challenges', 'wins', 'DEVELOP', 'DESIGN', 'DATA_SCIENCE', 'QA', 'COPILOT', 'createdAt',
5656
'updatedAt', 'createdBy', 'updatedBy']
5757

5858
const LEGACY_STATS_READ_SOURCE = 'legacy'
@@ -380,7 +380,6 @@ function normalizeChallengeSourceTrack (value) {
380380

381381
/**
382382
* Check whether a challenge source track is Quality Assurance.
383-
* QA Challenge results are surfaced in the legacy Testing bucket.
384383
* @param {*} value raw challenge track label, enum value, or abbreviation
385384
* @returns {boolean} true when the source track is QA
386385
*/
@@ -527,8 +526,8 @@ function buildBaseRatingJob (challenge, source) {
527526
if (source === RATING_SOURCE_QUALITY_ASSURANCE_CHALLENGE) {
528527
return {
529528
source,
530-
trackId: TRACK_NAMES.DEVELOP,
531-
typeId: TYPE_NAMES.BUG_HUNT
529+
trackId: TRACK_NAMES.QA,
530+
typeId: TYPE_NAMES.CHALLENGE
532531
}
533532
}
534533

@@ -609,7 +608,7 @@ async function fetchChallengeResultParticipantIds (reviewDbClient, challengeId)
609608

610609
/**
611610
* Fetch placement winner participants from challenge-api for completed
612-
* Development/Data Science rating rerates. This covers challenges where winners
611+
* Development/Data Science/QA rating rerates. This covers challenges where winners
613612
* can be assigned without a review-api challengeResult row for the same member.
614613
* @param {Object} challengeClient challenge Prisma client
615614
* @param {string|number} challengeId challenge identifier
@@ -759,8 +758,8 @@ async function rerateChallengeRatingJobForMember (challengeClient, reviewDbClien
759758
}
760759
} else if (job.source === RATING_SOURCE_QUALITY_ASSURANCE_CHALLENGE) {
761760
rerateOptions = {
762-
targetTrackName: TRACK_NAMES.DEVELOP,
763-
targetTypeName: TYPE_NAMES.BUG_HUNT,
761+
targetTrackName: TRACK_NAMES.QA,
762+
targetTypeName: TYPE_NAMES.CHALLENGE,
764763
challengeTrackNames: getQualityAssuranceChallengeSourceTrackNames(),
765764
challengeTypeNames: [TYPE_NAMES.CHALLENGE]
766765
}
@@ -994,8 +993,8 @@ function isMarathonMatchType (typeName) {
994993
/**
995994
* Resolve the public stats dimensions for a challenge-backed row.
996995
* Marathon Match rows are part of the public DATA_SCIENCE bucket even when
997-
* source challenge metadata uses a different track. QA Challenge rows retain
998-
* the legacy Testing surface under DEVELOP / BUG_HUNT.
996+
* source challenge metadata uses a different track. QA Challenge rows remain
997+
* under the first-class QA / Challenge dimension.
999998
* @param {Object} row row containing trackId and typeId
1000999
* @param {Object} dimensionLookup shared challenge dimension lookup
10011000
* @returns {Object} normalized track/type ids and names
@@ -1014,15 +1013,15 @@ function resolveStatsDimensionForChallengeRow (row, dimensionLookup) {
10141013

10151014
const trackName = resolveTrackNameFromLookup(dimensionLookup, row.trackId)
10161015
if (typeName === TYPE_NAMES.CHALLENGE && isQualityAssuranceChallengeSourceTrack(trackName)) {
1017-
const developTrackId = resolveTrackIdFromLookup(dimensionLookup, TRACK_NAMES.DEVELOP)
1018-
const bugHuntTypeId = resolveTypeIdFromLookup(dimensionLookup, TYPE_NAMES.BUG_HUNT)
1016+
const qaTrackId = resolveTrackIdFromLookup(dimensionLookup, TRACK_NAMES.QA)
1017+
const challengeTypeId = resolveTypeIdFromLookup(dimensionLookup, TYPE_NAMES.CHALLENGE)
10191018

1020-
if (developTrackId && bugHuntTypeId) {
1019+
if (qaTrackId && challengeTypeId) {
10211020
return {
1022-
trackId: developTrackId,
1023-
typeId: bugHuntTypeId,
1024-
trackName: TRACK_NAMES.DEVELOP,
1025-
typeName: TYPE_NAMES.BUG_HUNT
1021+
trackId: qaTrackId,
1022+
typeId: challengeTypeId,
1023+
trackName: TRACK_NAMES.QA,
1024+
typeName: TYPE_NAMES.CHALLENGE
10261025
}
10271026
}
10281027
}
@@ -1618,12 +1617,12 @@ function buildAggregatedStatsFromReviewResults (reviewRows, challengeMetadataByI
16181617

16191618
/**
16201619
* Check whether the unified history response should surface the supplied track.
1621-
* The public history contract currently exposes DEVELOPMENT, DESIGN, and DATA_SCIENCE groups.
1620+
* The public history contract currently exposes DEVELOPMENT, DESIGN, DATA_SCIENCE, and QA groups.
16221621
* @param {string|undefined} trackName canonical track label
16231622
* @returns {boolean} true when the track should be included in history responses
16241623
*/
16251624
function isSupportedUnifiedHistoryTrack (trackName) {
1626-
return _.includes([TRACK_NAMES.DEVELOP, TRACK_NAMES.DESIGN, TRACK_NAMES.DATA_SCIENCE], trackName)
1625+
return _.includes([TRACK_NAMES.DEVELOP, TRACK_NAMES.DESIGN, TRACK_NAMES.DATA_SCIENCE, TRACK_NAMES.QA], trackName)
16271626
}
16281627

16291628
/**
@@ -4353,7 +4352,7 @@ refreshMemberStats.schema = {
43534352
* rating dimensions. This includes the native challenge track/type rating when
43544353
* supported and any configured named rating paths whose tags/skills match the
43554354
* challenge, such as the default AI path. Quality Assurance Challenge rows are
4356-
* replayed into the legacy Testing bucket under DEVELOP / BUG_HUNT.
4355+
* replayed into the first-class QA / Challenge stats dimension.
43574356
* @param {Object} currentUser the user who performs operation
43584357
* @param {Object} data rerate payload containing the completed challenge id
43594358
* @returns {Object} summary of participants, rating jobs, updates, and per-member failures
@@ -4514,7 +4513,7 @@ rerateChallengeSubmitterRatings.schema = {
45144513

45154514
/**
45164515
* Trigger a DEVELOPMENT / Challenge, DATA_SCIENCE / Challenge,
4517-
* DATA_SCIENCE / MARATHON_MATCH, or configured tag- or skill-based rating path
4516+
* QA / Challenge, DATA_SCIENCE / MARATHON_MATCH, or configured tag- or skill-based rating path
45184517
* re-rating pass beginning with the supplied challenge.
45194518
* The relevant review-api results are reprocessed in chronological order and
45204519
* persisted into the existing unified rating tables for the member.
@@ -4572,6 +4571,20 @@ async function rerateMemberStats (currentUser, handle, data) {
45724571
challengeTypeNames: [TYPE_NAMES.CHALLENGE]
45734572
}
45744573
)
4574+
} else if (trackId === TRACK_NAMES.QA && typeId === TYPE_NAMES.CHALLENGE) {
4575+
result = await rerateDevTrack(
4576+
prisma,
4577+
challengeClient,
4578+
reviewDbClient,
4579+
member.userId,
4580+
payload.challengeId,
4581+
{
4582+
targetTrackName: TRACK_NAMES.QA,
4583+
targetTypeName: TYPE_NAMES.CHALLENGE,
4584+
challengeTrackNames: getQualityAssuranceChallengeSourceTrackNames(),
4585+
challengeTypeNames: [TYPE_NAMES.CHALLENGE]
4586+
}
4587+
)
45754588
} else if (trackId === TRACK_NAMES.DATA_SCIENCE && typeId === TYPE_NAMES.MARATHON_MATCH) {
45764589
result = await rerateMmTrack(
45774590
prisma,
@@ -4582,7 +4595,7 @@ async function rerateMemberStats (currentUser, handle, data) {
45824595
payload.challengeId
45834596
)
45844597
} else {
4585-
throw new errors.BadRequestError('Only DEVELOP / Challenge, DATA_SCIENCE / Challenge, and DATA_SCIENCE / MARATHON_MATCH rerates are currently supported.')
4598+
throw new errors.BadRequestError('Only DEVELOP / Challenge, DATA_SCIENCE / Challenge, QA / Challenge, and DATA_SCIENCE / MARATHON_MATCH rerates are currently supported.')
45864599
}
45874600

45884601
return {
@@ -4607,7 +4620,7 @@ rerateMemberStats.schema = {
46074620
data: Joi.object().keys({
46084621
challengeId: Joi.alternatives().try(Joi.string().uuid(), Joi.number().integer().strict()).required(),
46094622
ratingName: Joi.string(),
4610-
trackId: Joi.string().valid(TRACK_NAMES.DEVELOP, TRACK_NAMES.DATA_SCIENCE).insensitive(),
4623+
trackId: Joi.string().valid(TRACK_NAMES.DEVELOP, TRACK_NAMES.DATA_SCIENCE, TRACK_NAMES.QA).insensitive(),
46114624
typeId: Joi.string().valid(TYPE_NAMES.CHALLENGE, TYPE_NAMES.MARATHON_MATCH).insensitive()
46124625
}).required()
46134626
}

test/unit/DevelopRatingEngine.test.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const {
1717
const should = chai.should()
1818
const DEVELOP_TRACK_ID = 'track-develop-id'
1919
const DATA_SCIENCE_TRACK_ID = 'track-data-science-id'
20+
const QA_TRACK_ID = 'track-qa-id'
2021
const CHALLENGE_TYPE_ID = 'type-challenge-id'
2122
const CODE_TYPE_ID = 'type-code-id'
2223
const BUG_HUNT_TYPE_ID = 'type-bug-hunt-id'
@@ -364,7 +365,8 @@ function createChallengeClient (metadataById, winnerRows = []) {
364365
if (sql.includes('FROM "ChallengeTrack"')) {
365366
return [
366367
{ id: DEVELOP_TRACK_ID, name: 'Development', abbreviation: 'DEV', legacyId: null },
367-
{ id: DATA_SCIENCE_TRACK_ID, name: 'Data Science', abbreviation: 'DS', legacyId: null }
368+
{ id: DATA_SCIENCE_TRACK_ID, name: 'Data Science', abbreviation: 'DS', legacyId: null },
369+
{ id: QA_TRACK_ID, name: 'Quality Assurance', abbreviation: 'QA', legacyId: null }
368370
]
369371
}
370372

@@ -600,7 +602,7 @@ describe('develop rating engine unit tests', () => {
600602
members.state.rankRecalculationCalls[0].typeId.should.equal(CHALLENGE_TYPE_ID)
601603
})
602604

603-
it('rerateDevTrack should include QA ChallengeWinner placements in Testing rerates', async () => {
605+
it('rerateDevTrack should include QA ChallengeWinner placements in QA rerates', async () => {
604606
const targetUserId = toBigInt(89770374)
605607
const opponentUserId = toBigInt(100000039)
606608
const challengeId = 'qa-rating-jun-2'
@@ -650,8 +652,8 @@ describe('develop rating engine unit tests', () => {
650652
targetUserId,
651653
challengeId,
652654
{
653-
targetTrackName: 'DEVELOP',
654-
targetTypeName: 'BUG_HUNT',
655+
targetTrackName: 'QA',
656+
targetTypeName: 'Challenge',
655657
challengeTrackNames: ['QUALITY_ASSURANCE'],
656658
challengeTypeNames: ['Challenge']
657659
}
@@ -662,8 +664,8 @@ describe('develop rating engine unit tests', () => {
662664

663665
const statsRow = members.state.statsRows.find((row) =>
664666
String(row.userId) === String(targetUserId) &&
665-
row.trackId === DEVELOP_TRACK_ID &&
666-
row.typeId === BUG_HUNT_TYPE_ID
667+
row.trackId === QA_TRACK_ID &&
668+
row.typeId === CHALLENGE_TYPE_ID
667669
)
668670
should.exist(statsRow)
669671
statsRow.rating.should.equal(expectedTarget.rating)
@@ -673,16 +675,16 @@ describe('develop rating engine unit tests', () => {
673675

674676
const historyRow = findHistoryRow(members.state.historyRows, targetUserId, challengeId)
675677
should.exist(historyRow)
676-
historyRow.trackId.should.equal(DEVELOP_TRACK_ID)
677-
historyRow.typeId.should.equal(BUG_HUNT_TYPE_ID)
678+
historyRow.trackId.should.equal(QA_TRACK_ID)
679+
historyRow.typeId.should.equal(CHALLENGE_TYPE_ID)
678680
historyRow.newRating.should.equal(expectedTarget.rating)
679681
historyRow.mostRecent.should.equal(true)
680682

681683
const maxRatingRow = members.state.maxRatingRows.find((row) => String(row.userId) === String(targetUserId))
682684
should.exist(maxRatingRow)
683685
maxRatingRow.rating.should.equal(expectedTarget.rating)
684-
maxRatingRow.track.should.equal('DEVELOP')
685-
maxRatingRow.subTrack.should.equal('BUG_HUNT')
686+
maxRatingRow.track.should.equal('QA')
687+
maxRatingRow.subTrack.should.equal('Challenge')
686688
maxRatingRow.ratingColor.should.equal(getRatingColor(expectedTarget.rating))
687689
})
688690

0 commit comments

Comments
 (0)