diff --git a/__tests__/shared/components/SubmissionManagement/__snapshots__/SubmissionsTable.jsx.snap b/__tests__/shared/components/SubmissionManagement/__snapshots__/SubmissionsTable.jsx.snap index d8f5971b3..46edffe3d 100644 --- a/__tests__/shared/components/SubmissionManagement/__snapshots__/SubmissionsTable.jsx.snap +++ b/__tests__/shared/components/SubmissionManagement/__snapshots__/SubmissionsTable.jsx.snap @@ -25,6 +25,7 @@ exports[`Matches shallow shapshot 1`] = ` { }); }); +describe('belongsToActiveSubmissionPhase', () => { + test('allows deleting a checkpoint submission while the checkpoint phase is open', () => { + const phases = [ + { isOpen: true, name: 'Checkpoint Submission' }, + { isOpen: false, name: 'Submission' }, + ]; + + expect(belongsToActiveSubmissionPhase( + { id: 'checkpoint-submission', type: 'CHECKPOINT_SUBMISSION' }, + phases, + )).toBe(true); + expect(belongsToActiveSubmissionPhase( + { id: 'legacy-checkpoint', submissionType: 'checkpoint' }, + phases, + )).toBe(true); + }); + + test('blocks deleting a checkpoint submission once the submission phase opens', () => { + const phases = [ + { isOpen: false, name: 'Checkpoint Submission' }, + { isOpen: true, name: 'Submission' }, + ]; + + expect(belongsToActiveSubmissionPhase( + { id: 'checkpoint-submission', type: 'CHECKPOINT_SUBMISSION' }, + phases, + )).toBe(false); + expect(belongsToActiveSubmissionPhase( + { id: 'contest-submission', type: 'CONTEST_SUBMISSION' }, + phases, + )).toBe(true); + }); + + test('blocks deleting anything once every submission phase is closed', () => { + const phases = [ + { isOpen: false, name: 'Checkpoint Submission' }, + { isOpen: false, name: 'Submission' }, + { isOpen: true, name: 'Checkpoint Review' }, + { isOpen: true, name: 'Review' }, + ]; + + expect(belongsToActiveSubmissionPhase( + { id: 'checkpoint-submission', type: 'CHECKPOINT_SUBMISSION' }, + phases, + )).toBe(false); + expect(belongsToActiveSubmissionPhase( + { id: 'contest-submission', type: 'CONTEST_SUBMISSION' }, + phases, + )).toBe(false); + }); + + test('resolves to false for missing submissions or phases', () => { + expect(belongsToActiveSubmissionPhase(null, [{ isOpen: true, name: 'Submission' }])).toBe(false); + expect(belongsToActiveSubmissionPhase({ type: 'CONTEST_SUBMISSION' }, undefined)).toBe(false); + }); +}); + describe('getSubmissionLimitReachedMessage', () => { test('uses the requested singular limit message', () => { expect(getSubmissionLimitReachedMessage(1)).toBe( diff --git a/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx b/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx index 2eaa362d7..faf6902c3 100644 --- a/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx +++ b/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx @@ -40,7 +40,6 @@ export default function SubmissionManagement(props) { onShowDetails, challengeUrl, onlineReviewUrl, - submissionPhaseStartDate, onDownloadArtifacts, getSubmissionArtifacts, getSubmissionScores, @@ -191,7 +190,6 @@ export default function SubmissionManagement(props) { showDetails={showDetails} track={trackName} status={challenge.status} - submissionPhaseStartDate={submissionPhaseStartDate} {...componentConfig} /> ) @@ -251,5 +249,4 @@ SubmissionManagement.propTypes = { submissionLimitCheckPending: PT.bool, loadingSubmissions: PT.bool, challengeUrl: PT.string, - submissionPhaseStartDate: PT.string.isRequired, }; diff --git a/src/shared/components/SubmissionManagement/SubmissionsTable/index.jsx b/src/shared/components/SubmissionManagement/SubmissionsTable/index.jsx index a9ca5652f..74ccbef21 100644 --- a/src/shared/components/SubmissionManagement/SubmissionsTable/index.jsx +++ b/src/shared/components/SubmissionManagement/SubmissionsTable/index.jsx @@ -18,8 +18,8 @@ import _ from 'lodash'; import React, { useCallback, useState } from 'react'; import PT from 'prop-types'; import shortid from 'shortid'; -import moment from 'moment'; import { COMPETITION_TRACKS } from 'utils/tc'; +import { belongsToActiveSubmissionPhase } from 'utils/challenge-detail/submission-limit'; import ScreeningDetails from '../ScreeningDetails'; import DownloadArtifactsModal from '../DownloadArtifactsModal'; import Submission from '../Submission'; @@ -45,7 +45,6 @@ export default function SubmissionsTable(props) { onDownload, onShowDetails, status, - submissionPhaseStartDate, onDownloadArtifacts, getSubmissionArtifacts, getSubmissionScores, @@ -72,9 +71,6 @@ export default function SubmissionsTable(props) { )); } else { submissionObjects.forEach((subObject) => { - // submissionPhaseStartDate will be the start date of - // the current submission/checkpoint or empty string if any other phase - const TERMINAL_STATUSES = [ 'COMPLETED', 'FAILURE', @@ -93,8 +89,8 @@ export default function SubmissionsTable(props) { ? true : workflowRunsForSubmission.every(run => TERMINAL_STATUSES.includes(run.status)); - const allowDelete = submissionPhaseStartDate - && moment(subObject.submissionDate).isAfter(submissionPhaseStartDate); + // Submissions can only be removed while the phase that created them is still open. + const allowDelete = belongsToActiveSubmissionPhase(subObject, challenge.phases); const submission = ( @@ -233,6 +229,5 @@ SubmissionsTable.propTypes = { onDownloadArtifacts: PT.func, getSubmissionArtifacts: PT.func, status: PT.string.isRequired, - submissionPhaseStartDate: PT.string.isRequired, getSubmissionScores: PT.func, }; diff --git a/src/shared/containers/SubmissionManagement/index.jsx b/src/shared/containers/SubmissionManagement/index.jsx index 13f19e5b6..e9d0306ac 100644 --- a/src/shared/containers/SubmissionManagement/index.jsx +++ b/src/shared/containers/SubmissionManagement/index.jsx @@ -483,7 +483,6 @@ export class SubmissionManagementPageContainer extends React.Component { challengesUrl, deleting, loadingSubmissionsForChallengeId, - submissionPhaseStartDate, isLoadingChallenge, onCancelSubmissionDelete, onShowDetails, @@ -562,7 +561,6 @@ export class SubmissionManagementPageContainer extends React.Component { submissionLimitCheckPending={submissionLimitCheckPending} showDetails={showDetails} submissionWorkflowRuns={submissionWorkflowRuns} - submissionPhaseStartDate={submissionPhaseStartDate} {...smConfig} /> )} @@ -672,7 +670,6 @@ SubmissionManagementPageContainer.propTypes = { toBeDeletedId: PT.string, deletionSucceed: PT.bool, onSubmissionDeleteConfirmed: PT.func.isRequired, - submissionPhaseStartDate: PT.string.isRequired, }; function mapStateToProps(state, props) { @@ -682,9 +679,6 @@ function mapStateToProps(state, props) { mySubmissions = challengeId === mySubmissions.challengeId ? mySubmissions.v2 : null; - const allPhases = state.challenge.details.phases || []; - const submissionPhase = allPhases.find(phase => ['Submission', 'Checkpoint Submission'].includes(phase.name) && phase.isOpen) || {}; - return { challengeId: String(challengeId), challenge: state.challenge.details, @@ -698,8 +692,6 @@ function mapStateToProps(state, props) { state.challenge.loadingSubmissionsForChallengeId || '', mySubmissions, - submissionPhaseStartDate: submissionPhase.actualStartDate || submissionPhase.scheduledStartDate || '', - showDetails: state.page.submissionManagement.showDetails, submissionWorkflowRuns: state.page.submissionManagement.submissionWorkflowRuns, diff --git a/src/shared/utils/challenge-detail/submission-limit.js b/src/shared/utils/challenge-detail/submission-limit.js index 061a9332b..7a008d5fa 100644 --- a/src/shared/utils/challenge-detail/submission-limit.js +++ b/src/shared/utils/challenge-detail/submission-limit.js @@ -2,6 +2,7 @@ const SUBMISSION_LIMIT_METADATA_NAME = 'submissionLimit'; const CHECKPOINT_SUBMISSION_TYPE = 'CHECKPOINT_SUBMISSION'; const CONTEST_SUBMISSION_TYPE = 'CONTEST_SUBMISSION'; const FINAL_FIX_SUBMISSION_TYPE = 'STUDIO_FINAL_FIX_SUBMISSION'; +const SUBMISSION_PHASE_NAMES = ['Checkpoint Submission', 'Submission']; /** * Converts a metadata value to a positive integer submission limit. @@ -164,6 +165,33 @@ function normalizeSubmissionType(value) { return normalizedValue; } +/** + * Checks whether a submission was created by the currently open submission phase. + * + * Submitters may only remove a submission while the phase that produced it is still open. A + * checkpoint submission therefore stops being deletable as soon as the Checkpoint Submission + * phase ends, even though the Submission phase opens right after it, and nothing is deletable + * once every submission phase is closed (screening, review, appeals, ...). + * + * @param {Object} submission Member submission, using the V6 `type` or legacy `submissionType`. + * @param {Array} phases Challenge phases. + * @return {Boolean} Whether the submission belongs to the currently open submission phase. + * @throws Does not throw; missing submissions or phases resolve to false. + */ +export function belongsToActiveSubmissionPhase(submission, phases) { + const challengePhases = Array.isArray(phases) ? phases : []; + const hasOpenSubmissionPhase = challengePhases.some(phase => ( + phase && phase.isOpen && SUBMISSION_PHASE_NAMES.includes(phase.name) + )); + + if (!submission || !hasOpenSubmissionPhase) { + return false; + } + + return normalizeSubmissionType(submission.type || submission.submissionType) + === getActiveSubmissionType(challengePhases); +} + /** * Checks whether concept submission limits apply to a V6 submission type. *