diff --git a/src/shared/components/challenge-detail/Specification/styles.scss b/src/shared/components/challenge-detail/Specification/styles.scss index 13db0603c..8de28d4cf 100644 --- a/src/shared/components/challenge-detail/Specification/styles.scss +++ b/src/shared/components/challenge-detail/Specification/styles.scss @@ -143,15 +143,23 @@ $tc-link-visited: #0c4e98; font-size: 13px; color: $tc-black; line-height: 20px; - margin: (2 * $base-unit) 0 (3 * $base-unit); - padding: 3 * $base-unit; - display: block; + margin: 0; + padding: 0 6px; + display: inline; } pre { + margin: (2 * $base-unit) 0 (3 * $base-unit); overflow-x: scroll; } + pre code { + display: block; + margin: 0; + padding: 3 * $base-unit; + white-space: pre; + } + ol { @include roboto-regular; @@ -385,8 +393,6 @@ $tc-link-visited: #0c4e98; } code { - white-space: pre; - margin: 10px 0 15px; background: $tc-gray-neutral-light; border: 1px solid silver; border-radius: 6px; @@ -395,8 +401,22 @@ $tc-link-visited: #0c4e98; font-size: 13px; color: $tc-black; line-height: 20px; - padding: 15px; + padding: 0 6px; + margin: 0; + display: inline; + white-space: pre-wrap; + } + + pre { + margin: 10px 0 15px; + overflow-x: scroll; + } + + pre code { display: block; + padding: 15px; + margin: 0; + white-space: pre; } sub { @@ -422,10 +442,6 @@ $tc-link-visited: #0c4e98; color: $tc-gray-90; line-height: 25px; } - - pre { - overflow-x: scroll; - } } } diff --git a/src/shared/utils/mm-review-summations.js b/src/shared/utils/mm-review-summations.js index 282c6effe..dd9abe2dc 100644 --- a/src/shared/utils/mm-review-summations.js +++ b/src/shared/utils/mm-review-summations.js @@ -50,7 +50,10 @@ function getSummationRating(summation) { return _.isNil(rating) ? null : rating; } -function ensureSubmissionEntry(existingEntry, { submissionId, timestamp, timestampValue }) { +function ensureSubmissionEntry( + existingEntry, + { submissionId, timestamp, timestampValue }, +) { const baseEntry = { submissionId, submissionTime: timestamp || null, @@ -81,7 +84,10 @@ function ensureSubmissionEntry(existingEntry, { submissionId, timestamp, timesta ...existingEntry, submissionId: existingEntry.submissionId || submissionId, submissionTime: existingEntry.submissionTime || baseEntry.submissionTime, - isLatest: existingEntry.isLatest === undefined ? baseEntry.isLatest : existingEntry.isLatest, + isLatest: + existingEntry.isLatest === undefined + ? baseEntry.isLatest + : existingEntry.isLatest, status: existingEntry.status || 'completed', reviewSummations, reviewSummation, @@ -97,7 +103,13 @@ function ensureSubmissionEntry(existingEntry, { submissionId, timestamp, timesta }; } -function mergeScoreData(meta, currentValue, score, timestampValue, options = {}) { +function mergeScoreData( + meta, + currentValue, + score, + timestampValue, + options = {}, +) { const { allowOlderTimestampUpdate = true } = options; const nextMeta = { ...meta }; let nextValue = currentValue; @@ -107,11 +119,18 @@ function mergeScoreData(meta, currentValue, score, timestampValue, options = {}) nextMeta.score = score; nextValue = _.isNil(score) ? null : score; } else if (timestampValue === nextMeta.timestamp) { - if (!_.isNil(score) && (_.isNil(nextMeta.score) || score > nextMeta.score)) { + if ( + !_.isNil(score) + && (_.isNil(nextMeta.score) || score > nextMeta.score) + ) { nextMeta.score = score; nextValue = score; } - } else if (allowOlderTimestampUpdate && _.isNil(nextValue) && !_.isNil(score)) { + } else if ( + allowOlderTimestampUpdate + && _.isNil(nextValue) + && !_.isNil(score) + ) { nextMeta.timestamp = timestampValue; nextMeta.score = score; nextValue = score; @@ -123,15 +142,18 @@ function mergeScoreData(meta, currentValue, score, timestampValue, options = {}) }; } -function updateSubmissionEntry(existingEntry, { - submissionId, - timestamp, - timestampValue, - normalizedScore, - summation, - isProvisional, - isLatest, -}) { +function updateSubmissionEntry( + existingEntry, + { + submissionId, + timestamp, + timestampValue, + normalizedScore, + summation, + isProvisional, + isLatest, + }, +) { const baseEntry = ensureSubmissionEntry(existingEntry, { submissionId, timestamp, @@ -192,28 +214,55 @@ function updateSubmissionEntry(existingEntry, { }; } -function assignRanks(members, scoreKey, rankKey) { +function assignRanks(members, scoreKey, rankKey, options = {}) { + const { tieBreaker } = options; + const rankedEntries = members .map(member => ({ key: `${member.memberId || member.member || ''}`, score: member[scoreKey], + tieBreaker: tieBreaker ? tieBreaker(member) : null, })) .filter(entry => !_.isNil(entry.score)) - .sort((a, b) => b.score - a.score); + .sort((a, b) => { + const scoreDiff = b.score - a.score; + if (scoreDiff !== 0) { + return scoreDiff; + } + if (tieBreaker) { + const aTieValue = _.isNil(a.tieBreaker) + ? Number.POSITIVE_INFINITY + : a.tieBreaker; + const bTieValue = _.isNil(b.tieBreaker) + ? Number.POSITIVE_INFINITY + : b.tieBreaker; + if (aTieValue !== bTieValue) { + return aTieValue - bTieValue; + } + } + return 0; + }); - let processed = 0; - let previousScore = null; - let currentRank = 0; const rankByKey = new Map(); - rankedEntries.forEach((entry) => { - processed += 1; - if (previousScore === null || entry.score !== previousScore) { - currentRank = processed; - previousScore = entry.score; - } - rankByKey.set(entry.key, currentRank); - }); + if (tieBreaker) { + rankedEntries.forEach((entry, index) => { + rankByKey.set(entry.key, index + 1); + }); + } else { + let processed = 0; + let previousScore = null; + let currentRank = 0; + + rankedEntries.forEach((entry) => { + processed += 1; + if (previousScore === null || entry.score !== previousScore) { + currentRank = processed; + previousScore = entry.score; + } + rankByKey.set(entry.key, currentRank); + }); + } return members.map((member) => { const key = `${member.memberId || member.member || ''}`; @@ -243,27 +292,34 @@ function createStatisticsSubmission({ }; } -function updateStatisticsSubmission(submission, { - timestamp, - timestampValue, - score, -}) { +function updateStatisticsSubmission( + submission, + { timestamp, timestampValue, score }, +) { const base = { ...submission, - meta: submission.meta ? { ...submission.meta } : { timestamp: -Infinity, score: null }, + meta: submission.meta + ? { ...submission.meta } + : { timestamp: -Infinity, score: null }, }; const previousMeta = base.meta; - const { meta, value } = mergeScoreData(previousMeta, base.score, score, timestampValue, { - allowOlderTimestampUpdate: false, - }); + const { meta, value } = mergeScoreData( + previousMeta, + base.score, + score, + timestampValue, + { + allowOlderTimestampUpdate: false, + }, + ); const hasNewerTimestamp = meta.timestamp > previousMeta.timestamp; return { ...base, - created: hasNewerTimestamp ? (timestamp || base.created) : base.created, - createdAt: hasNewerTimestamp ? (timestamp || base.createdAt) : base.createdAt, + created: hasNewerTimestamp ? timestamp || base.created : base.created, + createdAt: hasNewerTimestamp ? timestamp || base.createdAt : base.createdAt, score: value, meta, }; @@ -303,24 +359,37 @@ export function buildMmSubmissionData(reviewSummations = []) { memberEntry.rating = rating; } - const rawSubmissionId = _.get(summation, 'submissionId', _.get(summation, 'id')); - const submissionId = rawSubmissionId ? _.toString(rawSubmissionId) : `unknown-${handle}-${index}`; + const rawSubmissionId = _.get( + summation, + 'submissionId', + _.get(summation, 'id'), + ); + const submissionId = rawSubmissionId + ? _.toString(rawSubmissionId) + : `unknown-${handle}-${index}`; const timestamp = getSummationTimestamp(summation); const timestampValue = toTimestampValue(timestamp); - const normalizedScore = normalizeScoreValue(_.get(summation, 'aggregateScore')); + const normalizedScore = normalizeScoreValue( + _.get(summation, 'aggregateScore'), + ); const isProvisional = Boolean(summation.isProvisional); - const isLatest = _.isNil(summation.isLatest) ? null : Boolean(summation.isLatest); + const isLatest = _.isNil(summation.isLatest) + ? null + : Boolean(summation.isLatest); - const updatedEntry = updateSubmissionEntry(memberEntry.submissionsMap.get(submissionId), { - submissionId, - timestamp, - timestampValue, - normalizedScore, - summation, - isProvisional, - isLatest, - }); + const updatedEntry = updateSubmissionEntry( + memberEntry.submissionsMap.get(submissionId), + { + submissionId, + timestamp, + timestampValue, + normalizedScore, + summation, + isProvisional, + isLatest, + }, + ); memberEntry.submissionsMap.set(submissionId, updatedEntry); }); @@ -331,22 +400,42 @@ export function buildMmSubmissionData(reviewSummations = []) { submissionId: submission.submissionId, submissionTime: submission.submissionTime, isLatest: submission.isLatest, - provisionalScore: _.isNil(submission.provisionalScore) ? null : submission.provisionalScore, - finalScore: _.isNil(submission.finalScore) ? null : submission.finalScore, + provisionalScore: _.isNil(submission.provisionalScore) + ? null + : submission.provisionalScore, + finalScore: _.isNil(submission.finalScore) + ? null + : submission.finalScore, status: submission.status || 'completed', reviewSummations: [...submission.reviewSummations], reviewSummation: [...submission.reviewSummations], })) - .sort((a, b) => toTimestampValue(b.submissionTime) - toTimestampValue(a.submissionTime)); + .sort( + (a, b) => toTimestampValue(b.submissionTime) + - toTimestampValue(a.submissionTime), + ); const hasLatestFlag = submissions.some(s => !_.isNil(s.isLatest)); - const latestSubmissions = hasLatestFlag ? submissions.filter(s => s.isLatest) : submissions; - const candidates = latestSubmissions.length ? latestSubmissions : submissions; - const bestProvisionalScore = _.chain(candidates) - .map(s => (_.isNil(s.provisionalScore) ? null : s.provisionalScore)) - .filter(s => !_.isNil(s)) - .max() - .value() || null; + const latestSubmissions = hasLatestFlag + ? submissions.filter(s => s.isLatest) + : submissions; + const candidates = latestSubmissions.length + ? latestSubmissions + : submissions; + const latestSubmissionForRanking = (latestSubmissions.length + ? latestSubmissions + : submissions)[0] || null; + // Provisional ranks should be based solely on the most recent submission, + // not the best historical one. + const bestProvisionalScore = normalizeScoreValue( + _.get(latestSubmissionForRanking, 'provisionalScore'), + ); + const latestProvisionalTimestampValue = toTimestampValue( + _.get(latestSubmissionForRanking, 'submissionTime'), + ); + const bestProvisionalTimestamp = _.isFinite(latestProvisionalTimestampValue) + ? latestProvisionalTimestampValue + : null; const bestFinalScore = _.chain(candidates) .map(s => (_.isNil(s.finalScore) ? null : s.finalScore)) .filter(s => !_.isNil(s)) @@ -354,13 +443,18 @@ export function buildMmSubmissionData(reviewSummations = []) { .value() || null; const rating = _.isNil(memberEntry.rating) ? null : memberEntry.rating; - const memberId = memberEntry.memberId ? _.toString(memberEntry.memberId) : null; - - const registrant = memberId ? { - userId: memberId, - memberHandle: memberEntry.handle === 'unknown' ? null : memberEntry.handle, - rating, - } : null; + const memberId = memberEntry.memberId + ? _.toString(memberEntry.memberId) + : null; + + const registrant = memberId + ? { + userId: memberId, + memberHandle: + memberEntry.handle === 'unknown' ? null : memberEntry.handle, + rating, + } + : null; return { member: memberEntry.handle, @@ -371,15 +465,37 @@ export function buildMmSubmissionData(reviewSummations = []) { finalRank: null, submissions, bestProvisionalScore, + bestProvisionalTimestamp, bestFinalScore, }; }); - const withProvisionalRanks = assignRanks(members, 'bestProvisionalScore', 'provisionalRank'); - const withFinalRanks = assignRanks(withProvisionalRanks, 'bestFinalScore', 'finalRank'); + const withProvisionalRanks = assignRanks( + members, + 'bestProvisionalScore', + 'provisionalRank', + { + tieBreaker: (entry) => { + const timestamp = _.get(entry, 'bestProvisionalTimestamp'); + return _.isFinite(timestamp) ? timestamp : Number.POSITIVE_INFINITY; + }, + }, + ); + const withFinalRanks = assignRanks( + withProvisionalRanks, + 'bestFinalScore', + 'finalRank', + ); return withFinalRanks - .map(({ bestProvisionalScore, bestFinalScore, ...rest }) => rest) + .map( + ({ + bestProvisionalScore, + bestFinalScore, + bestProvisionalTimestamp, + ...rest + }) => rest, + ) .sort((a, b) => { if (!_.isNil(a.finalRank) && !_.isNil(b.finalRank)) { return a.finalRank - b.finalRank; @@ -435,13 +551,23 @@ export function buildStatisticsData(reviewSummations = []) { const timestampValue = toTimestampValue(timestamp); const score = normalizeScoreValue(_.get(summation, 'aggregateScore')); - const rawSubmissionId = _.get(summation, 'submissionId', _.get(summation, 'id')); - const submissionId = rawSubmissionId ? _.toString(rawSubmissionId) : `unknown-${handle}-${index}`; + const rawSubmissionId = _.get( + summation, + 'submissionId', + _.get(summation, 'id'), + ); + const submissionId = rawSubmissionId + ? _.toString(rawSubmissionId) + : `unknown-${handle}-${index}`; const existingSubmission = entry.submissionsMap.get(submissionId); const updatedSubmission = existingSubmission - ? updateStatisticsSubmission(existingSubmission, { timestamp, timestampValue, score }) + ? updateStatisticsSubmission(existingSubmission, { + timestamp, + timestampValue, + score, + }) : createStatisticsSubmission({ submissionId, timestamp, @@ -462,7 +588,9 @@ export function buildStatisticsData(reviewSummations = []) { createdAt: submission.createdAt, score: submission.score, })) - .sort((a, b) => toTimestampValue(b.createdAt) - toTimestampValue(a.createdAt)), + .sort( + (a, b) => toTimestampValue(b.createdAt) - toTimestampValue(a.createdAt), + ), })); }