diff --git a/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx b/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
index 6f39e5a31..7bbd825b9 100644
--- a/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
+++ b/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
@@ -127,6 +127,24 @@ describe('getSubmissionTestProgress', () => {
status: 'FAILED',
});
});
+
+ it('surfaces cancelled scoring runs superseded by a newer submission', () => {
+ expect(getSubmissionTestProgress({
+ reviewSummations: [
+ {
+ metadata: {
+ testProcess: 'provisional',
+ testProgress: 1,
+ testStatus: 'CANCELLED',
+ },
+ },
+ ],
+ })).toEqual({
+ process: 'provisional',
+ progressPercent: '100%',
+ status: 'CANCELLED',
+ });
+ });
});
describe('isActiveTestStatus', () => {
diff --git a/__tests__/shared/utils/challenge-detail/submission-status.test.js b/__tests__/shared/utils/challenge-detail/submission-status.test.js
index 88c7baa7f..95d18c70e 100644
--- a/__tests__/shared/utils/challenge-detail/submission-status.test.js
+++ b/__tests__/shared/utils/challenge-detail/submission-status.test.js
@@ -10,6 +10,7 @@ describe('getSubmissionStatus', () => {
})).toEqual({
hasReviewSummation: false,
isAccepted: false,
+ isCancelled: false,
isFailed: true,
});
});
@@ -28,6 +29,30 @@ describe('getSubmissionStatus', () => {
})).toEqual({
hasReviewSummation: false,
isAccepted: false,
+ isCancelled: false,
+ isFailed: false,
+ });
+ });
+
+ it('reports cancelled scoring runs instead of leaving them in preparing state', () => {
+ expect(getSubmissionStatus({
+ status: 'ACTIVE',
+ submissionId: 'submission-1',
+ reviewSummations: [
+ {
+ submissionId: 'submission-1',
+ isProvisional: true,
+ metadata: {
+ testProcess: 'provisional',
+ testProgress: 1,
+ testStatus: 'CANCELLED',
+ },
+ },
+ ],
+ })).toEqual({
+ hasReviewSummation: true,
+ isAccepted: true,
+ isCancelled: true,
isFailed: false,
});
});
diff --git a/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx b/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx
index 1d90e04bb..7c0a01281 100644
--- a/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx
+++ b/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx
@@ -146,10 +146,13 @@ class SubmissionsDetailView extends React.Component {
const { onCancel, submission, onSortChange } = this.props;
let { finalScore } = submission;
const { sortedSubmissions } = this.state;
- const { isAccepted, isFailed } = getSubmissionStatus(submission);
+ const { isAccepted, isCancelled, isFailed } = getSubmissionStatus(submission);
let finalStatusStyleName = 'status-in-queue';
let finalStatusLabel = 'Preparing';
- if (isAccepted) {
+ if (isCancelled) {
+ finalStatusStyleName = 'status-cancelled';
+ finalStatusLabel = 'Cancelled';
+ } else if (isAccepted) {
finalStatusStyleName = 'status-complete';
finalStatusLabel = 'Complete';
} else if (isFailed) {
diff --git a/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/styles.scss b/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/styles.scss
index 5ca773525..46ae018c2 100644
--- a/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/styles.scss
+++ b/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/styles.scss
@@ -257,6 +257,12 @@ button.header-sort {
font-weight: 500;
}
+.status-cancelled {
+ font-size: 14px;
+ color: #767676;
+ font-weight: 500;
+}
+
.mobile-header {
display: none;
font-weight: 600;
diff --git a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
index 647c82f7c..bab127b08 100644
--- a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
+++ b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
@@ -17,6 +17,7 @@ import DateSortIcon from 'assets/images/icon-date-sort.svg';
import SortIcon from 'assets/images/icon-sort.svg';
import Tooltip from 'components/Tooltip';
import IconFail from '../../icons/failed.svg';
+import IconTestCancelled from '../../icons/cancelled.svg';
import IconTestInProgress from '../../icons/clock.svg';
import IconTestSuccess from '../../icons/check-mark.svg';
import DownloadIcon from '../../../SubmissionManagement/Icons/IconSquareDownload.svg';
@@ -127,17 +128,19 @@ function normalizeTestProcess(value) {
*/
function normalizeTestStatus(value) {
const normalized = _.toUpper(_.toString(value || '').trim());
- if (['FAILED', 'IN PROGRESS', 'SUCCESS'].indexOf(normalized) >= 0) {
+ if (['CANCELLED', 'FAILED', 'IN PROGRESS', 'SUCCESS'].indexOf(normalized) >= 0) {
return normalized;
}
return undefined;
}
/**
- * Returns whether a Marathon Match test status still represents active testing.
+ * Returns whether a Marathon Match test status produced no score to display.
+ * Running tests have no score yet, and a run cancelled because the member
+ * submitted a newer solution never produces one.
*
* @param {String} status normalized test status from review summation metadata.
- * @returns {Boolean} true when scoring should remain hidden until testing completes.
+ * @returns {Boolean} true when scoring should stay hidden for the submission.
*/
export function isActiveTestStatus(status) {
const normalized = normalizeTestStatus(status);
@@ -257,6 +260,17 @@ function renderTestStatusIcon(status) {
);
}
+ if (status === 'CANCELLED') {
+ return (
+
+
+
+ );
+ }
return null;
}
@@ -662,10 +676,15 @@ class SubmissionsListView extends React.Component {
} else {
provisionalScore = 'N/A';
}
- const { isAccepted, isFailed } = getSubmissionStatus(mySubmission);
+ const {
+ isAccepted, isCancelled, isFailed,
+ } = getSubmissionStatus(mySubmission);
let statusStyleName = 'queue';
let statusLabel = 'Preparing';
- if (isAccepted) {
+ if (isCancelled) {
+ statusStyleName = 'cancelled';
+ statusLabel = 'Cancelled';
+ } else if (isAccepted) {
statusStyleName = 'accepted';
statusLabel = 'Accepted';
} else if (isFailed) {
diff --git a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/styles.scss b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/styles.scss
index 4c6a23fa3..59667c62d 100644
--- a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/styles.scss
+++ b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/styles.scss
@@ -61,6 +61,16 @@
line-height: 22px;
}
+.cancelled {
+ color: #767676;
+
+ @include roboto-medium;
+
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 22px;
+}
+
.h2 {
@include roboto-bold;
@@ -395,6 +405,10 @@ button.column-1-1,
color: #ef476f;
}
+.test-status-cancelled {
+ color: #767676;
+}
+
.icon-search {
margin-left: 17px;
}
diff --git a/src/shared/components/challenge-detail/icons/cancelled.svg b/src/shared/components/challenge-detail/icons/cancelled.svg
new file mode 100644
index 000000000..75caf86b1
--- /dev/null
+++ b/src/shared/components/challenge-detail/icons/cancelled.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/shared/utils/challenge-detail/submission-status.js b/src/shared/utils/challenge-detail/submission-status.js
index eb27d54a6..57060c4fd 100644
--- a/src/shared/utils/challenge-detail/submission-status.js
+++ b/src/shared/utils/challenge-detail/submission-status.js
@@ -50,6 +50,19 @@ function hasFailedSubmissionStatus(submission) {
return status === 'failed' || status === 'failure';
}
+/**
+ * Returns whether a review summation reports a cancelled Marathon Match test run.
+ * Marathon Match scoring is cancelled when the member submits a newer solution
+ * while an earlier scorer is still running.
+ *
+ * @param {Object} summation review summation attached to a submission attempt.
+ * @returns {Boolean} true when the summation test status is cancelled.
+ */
+function hasCancelledTestStatus(summation) {
+ const status = _.toUpper(_.toString(_.get(summation, 'metadata.testStatus', '')).trim());
+ return status === 'CANCELLED';
+}
+
export function getSubmissionReviewSummations(submission) {
return collectReviewSummations(submission);
}
@@ -58,10 +71,12 @@ export function getSubmissionReviewSummations(submission) {
* Builds display status flags for a challenge submission attempt.
* Review summations indicate accepted scoring; when no accepted summation is
* present, failed scan or submission states should display as failed instead of
- * staying in the generic preparing state.
+ * staying in the generic preparing state. Cancelled scoring runs take precedence
+ * so a superseded Marathon Match submission is never shown as still preparing.
*
* @param {Object} submission submission attempt shown in challenge details.
- * @returns {{hasReviewSummation: Boolean, isAccepted: Boolean, isFailed: Boolean}} status flags.
+ * @returns {{hasReviewSummation: Boolean, isAccepted: Boolean, isCancelled: Boolean,
+ * isFailed: Boolean}} status flags.
*/
export function getSubmissionStatus(submission) {
const targetIdRaw = _.get(submission, 'submissionId', _.get(submission, 'id', null));
@@ -93,12 +108,15 @@ export function getSubmissionStatus(submission) {
return hasFlag || type === 'provisional' || type === 'final';
});
+ const isCancelled = reviewSummations.some(hasCancelledTestStatus);
+
const isFailed = !isAccepted
&& (hasVirusScanFailure(submission) || hasFailedSubmissionStatus(submission));
return {
hasReviewSummation,
isAccepted,
+ isCancelled,
isFailed,
};
}