Skip to content

Commit a683a86

Browse files
authored
Merge pull request #152 from topcoder-platform/PM-5793-1
PM-5793: Restore downloaded profile ratings
2 parents a80568d + 78cafaf commit a683a86

4 files changed

Lines changed: 329 additions & 32 deletions

File tree

src/common/profileStats.ts

Lines changed: 127 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ const AI_ENGINEERING_TRACK_NAMES = new Set([
1616
'AI_ENGINEER',
1717
'AI_ENGINEERING'
1818
])
19+
const NATIVE_DATA_SCIENCE_SUBTRACK_NAMES = [
20+
'Challenge',
21+
'MARATHON_MATCH'
22+
]
23+
const NATIVE_DATA_SCIENCE_STATS_KEYS = new Set([
24+
...NATIVE_DATA_SCIENCE_SUBTRACK_NAMES,
25+
'SRM',
26+
'challenges',
27+
'mostRecentEventDate',
28+
'mostRecentEventName',
29+
'mostRecentSubmission',
30+
'wins'
31+
])
1932

2033
/**
2134
* Return a finite numeric value without coercing strings or nullish values.
@@ -134,13 +147,9 @@ function getAIEngineeringSource (stats) {
134147
subTrack: { ...subTrack, name },
135148
trackName: 'DATA_SCIENCE'
136149
}))
137-
.sort((left, right) => (
138-
(getFiniteNumber(right.subTrack.rank && right.subTrack.rank.rating) ?? 0) -
139-
(getFiniteNumber(left.subTrack.rank && left.subTrack.rank.rating) ?? 0)
140-
))
141150

142151
if (dataScienceCandidates.length > 0) {
143-
return dataScienceCandidates[0]
152+
return getDataScienceSummarySource(dataScienceCandidates)
144153
}
145154

146155
const topLevelName = ['AI_ENGINEERING', 'AI', 'AI_ENGINEER']
@@ -264,11 +273,68 @@ function getDevelopmentTrackSummary (sources, statsHistory) {
264273
}
265274
}
266275

276+
/**
277+
* Pick the Data Science subtrack whose rating Profiles displays.
278+
* Rating, percentile, and challenge count are descending tie breakers.
279+
* @param {Array<Object>} sources active rated Data Science sources
280+
* @returns {Object|undefined} source with the strongest visible rating
281+
*/
282+
function getDataScienceSummarySource (sources) {
283+
return [...sources].sort((left, right) => {
284+
const leftRank = left.subTrack.rank || {}
285+
const rightRank = right.subTrack.rank || {}
286+
287+
return (getFiniteNumber(rightRank.rating) ?? 0) - (getFiniteNumber(leftRank.rating) ?? 0) ||
288+
(getFiniteNumber(rightRank.percentile) ?? 0) - (getFiniteNumber(leftRank.percentile) ?? 0) ||
289+
(getFiniteNumber(right.subTrack.challenges) ?? 0) - (getFiniteNumber(left.subTrack.challenges) ?? 0)
290+
})[0]
291+
}
292+
293+
/**
294+
* Build the independently rated, non-native Data Science rows shown by Profiles.
295+
* Native Challenge, Marathon Match, and SRM rows are handled by their parent
296+
* tracks, while AI Engineering aliases remain grouped under Development.
297+
* @param {Object} stats member stats response for one public group
298+
* @param {Object|undefined} statsHistory member stats history response for the same group
299+
* @returns {Array<{trackName: string, rating: number, wins: number, submissions: number, challenges: number}>} custom rated rows
300+
*/
301+
function getDataScienceRatingPathRows (stats, statsHistory) {
302+
const dataScienceStats: Record<string, any> = stats.DATA_SCIENCE
303+
if (!dataScienceStats || typeof dataScienceStats !== 'object') {
304+
return []
305+
}
306+
307+
return Object.entries(dataScienceStats)
308+
.filter(([name, subTrack]) => (
309+
!NATIVE_DATA_SCIENCE_STATS_KEYS.has(name) &&
310+
!isAIEngineeringTrackName(name) &&
311+
subTrack && typeof subTrack === 'object' &&
312+
getFiniteNumber(subTrack.rank && subTrack.rank.rating) !== undefined
313+
))
314+
.sort(([, left], [, right]) => (
315+
(getFiniteNumber(right.wins) ?? 0) - (getFiniteNumber(left.wins) ?? 0) ||
316+
(getSubTrackDisplaySubmissionCount(right) ?? 0) - (getSubTrackDisplaySubmissionCount(left) ?? 0)
317+
))
318+
.map(([name, subTrack]) => {
319+
const source = {
320+
subTrack: { ...subTrack, name },
321+
trackName: 'DATA_SCIENCE'
322+
}
323+
324+
return {
325+
trackName: name,
326+
rating: getFiniteNumber(subTrack.rank && subTrack.rank.rating) ?? 0,
327+
...getSubTrackSummary(source, statsHistory),
328+
challenges: getFiniteNumber(subTrack.challenges) ?? 0
329+
}
330+
})
331+
}
332+
267333
/**
268334
* Convert member stats and history responses into downloaded-profile activity rows.
269-
* The PDF uses this mapper to match the Development, Design, Testing, and
270-
* Competitive Programming values shown by Profiles. Competitive Programming is
271-
* emitted only for active SRM stats; other Data Science activity is not relabeled.
335+
* The PDF uses this mapper to match the Development, Design, Testing, Data
336+
* Science, configured rating-path, and Competitive Programming values shown by
337+
* Profiles. Competitive Programming is emitted only for active SRM stats.
272338
* This function does not mutate its inputs or throw for missing response fields.
273339
* @param {Object|undefined} stats member stats response for one public group
274340
* @param {Object|undefined} statsHistory member stats history response for the same group
@@ -325,6 +391,26 @@ function buildProfileActivityStats (stats, statsHistory) {
325391
})
326392
}
327393

394+
const dataScienceStats: Record<string, any> = stats.DATA_SCIENCE || {}
395+
const dataScienceSources = NATIVE_DATA_SCIENCE_SUBTRACK_NAMES
396+
.filter(name => (
397+
dataScienceStats[name] &&
398+
typeof dataScienceStats[name] === 'object' &&
399+
(getFiniteNumber(dataScienceStats[name].challenges) ?? 0) > 0
400+
))
401+
.map(name => ({
402+
subTrack: { ...dataScienceStats[name], name },
403+
trackName: 'DATA_SCIENCE'
404+
}))
405+
if (dataScienceSources.length > 0) {
406+
const summarySource = getDataScienceSummarySource(dataScienceSources)
407+
result.push({
408+
trackName: 'Data Science',
409+
rating: getFiniteNumber(summarySource && summarySource.subTrack.rank && summarySource.subTrack.rank.rating) ?? 0,
410+
...getStandardTrackSummary(dataScienceSources, statsHistory)
411+
})
412+
}
413+
328414
const srmStats = stats.DATA_SCIENCE && stats.DATA_SCIENCE.SRM
329415
const competitions = getFiniteNumber(srmStats && srmStats.challenges) ?? 0
330416
if (competitions > 0) {
@@ -336,9 +422,41 @@ function buildProfileActivityStats (stats, statsHistory) {
336422
})
337423
}
338424

425+
result.push(...getDataScienceRatingPathRows(stats, statsHistory))
426+
339427
return result
340428
}
341429

430+
/**
431+
* Resolve stats requests and build downloaded-profile activity rows.
432+
* Member stats are required; history is optional and falls back to aggregate
433+
* counters when its request fails. Promise.all is compatible with the Bluebird
434+
* global used by the application bootstrap.
435+
* @param {Promise<Array<Object>>} statsRequest member stats request
436+
* @param {Promise<Array<Object>>} historyRequest member stats history request
437+
* @param {Function} [onHistoryFailure] optional history error callback
438+
* @returns {Promise<Array<{trackName: string, wins: number, submissions?: number, challenges?: number, rating?: number, competitions?: number}>>} PDF activity rows
439+
* @throws {*} when the required member stats request fails
440+
*/
441+
async function buildProfileActivityStatsFromRequests (statsRequest, historyRequest, onHistoryFailure) {
442+
const safeHistoryRequest = Promise.resolve(historyRequest).catch((error) => {
443+
if (typeof onHistoryFailure === 'function') {
444+
onHistoryFailure(error)
445+
}
446+
return []
447+
})
448+
const [statsResult, historyResult] = await Promise.all([
449+
statsRequest,
450+
safeHistoryRequest
451+
])
452+
453+
return buildProfileActivityStats(
454+
Array.isArray(statsResult) ? statsResult[0] : undefined,
455+
Array.isArray(historyResult) ? historyResult[0] : undefined
456+
)
457+
}
458+
342459
module.exports = {
343-
buildProfileActivityStats
460+
buildProfileActivityStats,
461+
buildProfileActivityStatsFromRequests
344462
}

src/common/profileTemplate.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ function createCategorySkillsBlock (categoryName, skillNames) {
297297
}
298298

299299
/**
300-
* Build the PDF template for member profile
300+
* Build the PDF template for a member profile, including rated activity rows.
301301
* @param {Object} pdfData the aggregated PDF data
302302
* @returns {Object} React element tree
303303
*/
@@ -515,13 +515,14 @@ function buildProfileTemplate (pdfData) {
515515
const statsItems = topcoderActivity.statsByTrack.map((stat, index) => {
516516
const isCompetitiveProgramming = stat.trackName === 'Competitive Programming'
517517
const rating = stat.rating == null ? 0 : stat.rating
518+
const hasTrackRating = !isCompetitiveProgramming && rating > 0
518519
const wins = stat.wins == null ? 0 : stat.wins
519520
const competitions = stat.competitions == null ? 0 : stat.competitions
520521
const submissions = stat.submissions == null ? 0 : stat.submissions
521522
const challenges = stat.challenges == null ? 0 : stat.challenges
522523
const valueText = isCompetitiveProgramming
523524
? `${rating} rating, ${wins} wins, ${competitions} competitions`
524-
: `${wins} ${wins === 1 ? 'win' : 'wins'}, ${submissions} ${submissions === 1 ? 'submission' : 'submissions'}, ${challenges} ${challenges === 1 ? 'challenge' : 'challenges'}`
525+
: `${hasTrackRating ? `${rating} rating, ` : ''}${wins} ${wins === 1 ? 'win' : 'wins'}, ${submissions} ${submissions === 1 ? 'submission' : 'submissions'}, ${challenges} ${challenges === 1 ? 'challenge' : 'challenges'}`
525526
return React.createElement(
526527
Text,
527528
{ key: `stats-track-${index}`, style: styles.activityItem },

src/services/MemberService.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const fileTypeChecker = require('file-type-checker')
2323
const sharp = require('sharp')
2424
const { bufferContainsScript } = require('../common/image')
2525
const { htmlToText } = require('../common/htmlUtils')
26-
const { buildProfileActivityStats } = require('../common/profileStats')
26+
const { buildProfileActivityStatsFromRequests } = require('../common/profileStats')
2727
const countryCallingCodes = require('country-calling-code')
2828
const prismaHelper = require('../common/prismaHelper')
2929
const prismaManager = require('../common/prisma')
@@ -1758,30 +1758,21 @@ async function getMemberRoles (userId) {
17581758

17591759
/**
17601760
* Fetch the member stats and history used by Profiles and map them for the PDF.
1761-
* Failures are logged and return no activity rows so profile generation can continue.
1761+
* Required stats failures are logged and return no activity rows so profile generation
1762+
* can continue; optional history failures fall back to aggregate counters.
17621763
* @param {Object} currentUser the user who performs the profile download
17631764
* @param {String} handle member handle
17641765
* @returns {Promise<Array<{ trackName: string, wins: number, submissions?: number, challenges?: number, rating?: number, competitions?: number }>>}
17651766
*/
17661767
async function fetchMemberStatsByTrack (currentUser, handle) {
17671768
try {
17681769
const StatisticsService = require('./StatisticsService')
1769-
const [statsOutcome, historyOutcome] = await Promise.allSettled([
1770+
const statsByTrack = await buildProfileActivityStatsFromRequests(
17701771
StatisticsService.getMemberStats(currentUser, handle, {}),
1771-
StatisticsService.getHistoryStats(currentUser, handle, {})
1772-
])
1773-
if (statsOutcome.status === 'rejected') {
1774-
throw statsOutcome.reason
1775-
}
1776-
if (historyOutcome.status === 'rejected') {
1777-
logger.warn(`fetchMemberStatsByTrack history lookup failed for ${handle}: ${historyOutcome.reason.message}`)
1778-
}
1779-
const statsResult = statsOutcome.value
1780-
const historyResult = historyOutcome.status === 'fulfilled' ? historyOutcome.value : []
1781-
return buildProfileActivityStats(
1782-
Array.isArray(statsResult) ? statsResult[0] : undefined,
1783-
Array.isArray(historyResult) ? historyResult[0] : undefined
1772+
StatisticsService.getHistoryStats(currentUser, handle, {}),
1773+
error => logger.warn(`fetchMemberStatsByTrack history lookup failed for ${handle}: ${error.message}`)
17841774
)
1775+
return statsByTrack
17851776
} catch (err) {
17861777
logger.warn(`fetchMemberStatsByTrack failed for ${handle}: ${err.message}`)
17871778
return []

0 commit comments

Comments
 (0)