Skip to content
1 change: 0 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ workflows:
branches:
only:
- develop
- pm-3734

# Production builds are exectuted only on tagged commits to the
# master branch.
Expand Down
9 changes: 4 additions & 5 deletions src/common/profileTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -496,13 +496,12 @@ function buildProfileTemplate (pdfData) {
if (topcoderActivity.specialRole || topcoderActivity.achievements || hasStatsByTrack) {
const activityContent = [createSectionHeader('TOPCODER ACTIVITY')]

// Member stats by track first (Development: wins, submissions, challenges; Competitive Programming: rating, wins, competitions)
if (hasStatsByTrack) {
const statsItems = topcoderActivity.statsByTrack.map((stat, index) => {
const isCompetitiveProgramming = stat.trackName === 'Competitive Programming'
const valueText = isCompetitiveProgramming
? `${stat.rating ?? 0} rating, ${stat.wins ?? 0} wins, ${stat.competitions ?? 0} competitions`
: `${stat.wins ?? 0} wins, ${stat.submissions ?? 0} submissions, ${stat.challenges ?? 0} challenges`
const wins = stat.wins ?? 0
const submissions = stat.submissions ?? 0
const challenges = stat.challenges ?? 0
const valueText = `${wins} ${wins === 1 ? 'win' : 'wins'}, ${submissions} ${submissions === 1 ? 'submission' : 'submissions'}, ${challenges} ${challenges === 1 ? 'challenge' : 'challenges'}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The logic for pluralizing 'wins', 'submissions', and 'challenges' is repeated. Consider extracting this logic into a helper function to improve maintainability and reduce duplication.

return React.createElement(
Text,
{ key: `stats-track-${index}`, style: styles.activityItem },
Expand Down
81 changes: 22 additions & 59 deletions src/services/MemberService.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ const academyPrisma = prismaManager.getAcademyClient()
const resourcesPrisma = prismaManager.getResourcesClient()
const engagementsPrisma = prismaManager.getEngagementsClient()
const profilePDFService = require('./ProfilePDFService')
const StatisticsService = require('./StatisticsService')
const request = require('request')
const cityTimezones = require('city-timezones')
const moment = require('moment-timezone')
Expand Down Expand Up @@ -1515,11 +1514,11 @@ async function getMemberRoles (userId) {
}
}

/** Track enum to display name (for standard tracks: wins, submissions, challenges) */
/** Track enum to display name (wins, submissions, challenges) */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
The comment for TRACK_DISPLAY_NAMES no longer accurately describes the mapping, as it mentions 'wins, submissions, challenges' which are not directly related to the purpose of this constant. Consider updating the comment to reflect the actual purpose of the mapping.

const TRACK_DISPLAY_NAMES = {
DEVELOPMENT: 'Development',
DESIGN: 'Design',
DATA_SCIENCE: 'Competitive Programming',
DATA_SCIENCE: 'Data Science',
QUALITY_ASSURANCE: 'Quality Assurance'
}

Expand All @@ -1529,36 +1528,35 @@ const TRACK_DISPLAY_NAMES = {
* @param {Number} userId member userId
* @param {Object} challengesPrisma challenges Prisma client
* @param {Object} resourcesPrisma resources Prisma client
* @returns {Promise<Array<{ trackName: string, wins: number, submissions: number, challenges: number, rating?: number, competitions?: number }>>}
* @returns {Promise<Array<{ trackName: string, wins: number, submissions: number, challenges: number }>>}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The return type in the JSDoc comment for fetchMemberStatsByTrack has been updated to remove rating and competitions. Ensure that this change is intentional and that no other parts of the code rely on these fields being present in the returned data.

*/
async function fetchMemberStatsByTrack (userId, challengesPrisma, resourcesPrisma) {
const trackMap = {} // track enum -> { wins, submissions, challenges } or { rating, wins, competitions } for DATA_SCIENCE
const trackMap = {} // track enum -> { wins, submissions, challenges }

try {
const numUserId = typeof userId === 'bigint' ? helper.bigIntToNumber(userId) : userId

// 1) ChallengeWinner: wins (and submissions if same table) by track
const winners = await challengesPrisma.ChallengeWinner.findMany({
where: { userId: numUserId },
const winnerRows = await challengesPrisma.ChallengeWinner.findMany({
where: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The where clause for findMany now includes a filter on type with values PLACEMENT and PASSED_REVIEW. Verify that these are the only types needed for the logic and that no other types should be considered.

userId: numUserId,
type: { in: ['PLACEMENT', 'PASSED_REVIEW'] }
},
include: {
challenge: {
include: { track: true }
}
}
})

for (const w of winners) {
for (const w of winnerRows) {
const trackEnum = w.challenge?.track?.track
if (!trackEnum) continue
if (!trackMap[trackEnum]) {
const isDataScience = trackEnum === 'DATA_SCIENCE'
trackMap[trackEnum] = isDataScience
? { wins: 0, competitions: 0, rating: undefined }
: { wins: 0, submissions: 0, challenges: 0 }
trackMap[trackEnum] = { wins: 0, submissions: 0, challenges: 0 }
}
const row = trackMap[trackEnum]
if (row.wins !== undefined) row.wins += 1
if (row.submissions !== undefined) row.submissions += 1
if (w.type === 'PLACEMENT') row.wins += 1
if (w.type === 'PASSED_REVIEW') row.submissions += 1
}

// 2) Resources: registrations (distinct challenges) by track
Expand Down Expand Up @@ -1592,13 +1590,9 @@ async function fetchMemberStatsByTrack (userId, challengesPrisma, resourcesPrism
}
for (const [trackEnum, count] of Object.entries(challengesPerTrack)) {
if (!trackMap[trackEnum]) {
const isDataScience = trackEnum === 'DATA_SCIENCE'
trackMap[trackEnum] = isDataScience
? { wins: 0, competitions: count, rating: undefined }
: { wins: 0, submissions: 0, challenges: count }
trackMap[trackEnum] = { wins: 0, submissions: 0, challenges: count }
} else {
if (trackMap[trackEnum].challenges !== undefined) trackMap[trackEnum].challenges = count
if (trackMap[trackEnum].competitions !== undefined) trackMap[trackEnum].competitions = count
trackMap[trackEnum].challenges = count
}
}
}
Expand All @@ -1607,22 +1601,13 @@ async function fetchMemberStatsByTrack (userId, challengesPrisma, resourcesPrism
for (const [trackEnum, counts] of Object.entries(trackMap)) {
const trackName = TRACK_DISPLAY_NAMES[trackEnum] || trackEnum
const hasAny = Object.values(counts).some(v => typeof v === 'number' && v > 0)
if (!hasAny && (counts.rating == null || counts.rating === 0)) continue
if (trackEnum === 'DATA_SCIENCE') {
statsByTrack.push({
trackName,
rating: counts.rating ?? 0,
wins: counts.wins ?? 0,
competitions: counts.competitions ?? 0
})
} else {
statsByTrack.push({
trackName,
wins: counts.wins ?? 0,
submissions: counts.submissions ?? 0,
challenges: counts.challenges ?? 0
})
}
if (!hasAny) continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The check for hasAny now only considers numeric values greater than zero. Ensure that this logic change aligns with the intended behavior, especially if there were cases where zero values were previously significant.

statsByTrack.push({
trackName,
wins: counts.wins ?? 0,
submissions: counts.submissions ?? 0,
challenges: counts.challenges ?? 0
})
}
return statsByTrack
} catch (err) {
Expand Down Expand Up @@ -1736,28 +1721,6 @@ async function aggregatePDFData (currentUser, handle) {
logger.warn(`aggregatePDFData: statsByTrack failed for ${handle}: ${err.message}`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The removal of the StatisticsService call and related logic for merging Competitive Programming ratings means this data will no longer be included. Confirm that this removal is intentional and that the application does not require this data elsewhere.

}

// Merge Competitive Programming rating from stats endpoint (same source as GET /members/:handle/stats)
try {
const statsResult = await StatisticsService.getMemberStats(currentUser, handle, {})
const statsResponse = Array.isArray(statsResult) && statsResult.length > 0 ? statsResult[0] : null
if (statsResponse && statsResponse.DATA_SCIENCE) {
const ds = statsResponse.DATA_SCIENCE
const rating = (ds.SRM && ds.SRM.rank && ds.SRM.rank.rating != null)
? ds.SRM.rank.rating
: (ds.MARATHON_MATCH && ds.MARATHON_MATCH.rank && ds.MARATHON_MATCH.rank.rating != null)
? ds.MARATHON_MATCH.rank.rating
: 0
const cpEntry = statsByTrack.find(e => e.trackName === 'Competitive Programming')
if (cpEntry) {
cpEntry.rating = rating
} else if (rating > 0) {
statsByTrack.push({ trackName: 'Competitive Programming', rating, wins: 0, competitions: 0 })
}
}
} catch (err) {
logger.warn(`aggregatePDFData: getMemberStats for rating failed for ${handle}: ${err.message}`)
}

// Fetch certifications and courses
const { certifications, courses } = await fetchCertificationsAndCourses(userId)

Expand Down
10 changes: 10 additions & 0 deletions src/services/MemberTraitService.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,16 @@ async function getTraits (currentUser, handle, query) {
if (traitIds) {
result = _.filter(result, (item) => _.includes(traitIds, item.traitId))
}
// links in personalization are only for the profile owner (self)
if (!isSelf) {
_.forEach(result, (item) => {
if (item.traitId === 'personalization' && item.traits && item.traits.data) {
_.forEach(item.traits.data, (dataEntry) => {
delete dataEntry.links

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ performance]
The use of delete in a loop can lead to performance issues because it forces the JavaScript engine to re-optimize the object. Consider setting dataEntry.links to undefined instead, which is generally more performant.

})
}
})
}
// convert date time for traits data
_.filter(result, (item) => _.forEach(item.traits.data, function (value) {
if (value.hasOwnProperty('birthDate')) {
Expand Down
Loading