diff --git a/app/models/course/assessment/submission.rb b/app/models/course/assessment/submission.rb index d37d21872e..79d0da66f7 100644 --- a/app/models/course/assessment/submission.rb +++ b/app/models/course/assessment/submission.rb @@ -274,14 +274,17 @@ def force_submit_job_scheduled? # when neither applies, i.e. the submission is never force-submitted. The deadline component honours # the submitter's personalised timeline. Does not include the grace period. # - # A submission that has been unsubmitted by staff is exempt: the student is redoing it with explicit - # permission, so it has no deadline and is never force-submitted. + # Exempt from the deadline (returns nil) when the submission has been unsubmitted by staff (a + # permitted redo). The deadline is also ignored when the submission was created after it had already + # passed — students cannot do this (creation is blocked), but staff can start a test run at any time, + # and such a run should behave as if there were no end date rather than be finalised immediately. # # @return [Time, nil] def force_submit_at return nil if unsubmitted_at.present? deadline = assessment.submission_deadline_for(course_user) + deadline = nil if deadline && created_at > deadline time_limit_at = assessment.time_limit && (created_at + assessment.time_limit.minutes) [deadline, time_limit_at].compact.min end diff --git a/app/views/course/assessment/assessments/_assessment_actions.json.jbuilder b/app/views/course/assessment/assessments/_assessment_actions.json.jbuilder index 070b3150b6..6796f24722 100644 --- a/app/views/course/assessment/assessments/_assessment_actions.json.jbuilder +++ b/app/views/course/assessment/assessments/_assessment_actions.json.jbuilder @@ -33,9 +33,11 @@ elsif attempting_submission.present? elsif submitted_submission.present? status = 'submitted' action_url = edit_course_assessment_submission_path(current_course, assessment, submitted_submission) -elsif assessment.submission_deadline_passed_for?(current_course_user) +elsif !can_manage && assessment.submission_deadline_passed_for?(current_course_user) # End date passed on an assessment that disallows late submissions, and no submission exists: the - # student can no longer start one. No action URL — the frontend shows a disabled button. + # student can no longer start one. No action URL — the frontend shows a disabled button. Staff who + # can manage the assessment are exempt (they may create test submissions at any time), matching the + # create authorization. status = 'closed' else status = 'open' diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx b/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx index 2ef9b67990..d4bd305343 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx +++ b/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx @@ -341,20 +341,23 @@ const AssessmentForm = (props: AssessmentFormProps): JSX.Element => { /> )} - ( - - )} - /> + {/* The setting only has an effect with an end date, so it is hidden without one. */} + {endAt && ( + ( + + )} + /> + )} {t(translations.description)} diff --git a/spec/controllers/course/assessment/submission/late_submission_spec.rb b/spec/controllers/course/assessment/submission/late_submission_spec.rb index 3b503a3e29..572b4507bd 100644 --- a/spec/controllers/course/assessment/submission/late_submission_spec.rb +++ b/spec/controllers/course/assessment/submission/late_submission_spec.rb @@ -68,9 +68,11 @@ describe '#edit fail-safe for an overdue submission' do let(:end_at) { 20.minutes.ago } + # The student began before the deadline, which has since passed while they were still attempting. let!(:submission) do create(:submission, :attempting, assessment: assessment, - creator: student.user, course_user: student) + creator: student.user, course_user: student). + tap { |s| s.update_column(:created_at, 1.hour.ago) } end subject do @@ -103,6 +105,22 @@ expect(submission.reload).to be_attempting end end + + context 'when staff started a test run after the deadline had passed' do + let(:staff) { create(:course_teaching_assistant, course: course) } + # Created now, i.e. after the passed end date — the deadline does not apply to it. + let!(:submission) do + create(:submission, :attempting, assessment: assessment, + creator: staff.user, course_user: staff) + end + + before { controller_sign_in(controller, staff.user) } + + it 'does not force-submit the staff test submission' do + expect { subject }.not_to(change { submission.reload.workflow_state }) + expect(submission.reload).to be_attempting + end + end end describe '#edit late flag' do diff --git a/spec/jobs/course/assessment/submission/force_submit_timed_submission_job_spec.rb b/spec/jobs/course/assessment/submission/force_submit_timed_submission_job_spec.rb index a678ebac74..7403a04d08 100644 --- a/spec/jobs/course/assessment/submission/force_submit_timed_submission_job_spec.rb +++ b/spec/jobs/course/assessment/submission/force_submit_timed_submission_job_spec.rb @@ -12,9 +12,11 @@ create(:assessment, :with_mcq_question, course: course, end_at: end_at, is_late_submission_allowed: false) end + # The attempt was begun before the (now-passed) deadline. let!(:submission) do create(:submission, :attempting, assessment: assessment, - creator: student.user, course_user: student) + creator: student.user, course_user: student). + tap { |s| s.update_column(:created_at, 2.hours.ago) } end def run diff --git a/spec/jobs/course/assessment/submission/schedule_expiring_submissions_job_spec.rb b/spec/jobs/course/assessment/submission/schedule_expiring_submissions_job_spec.rb index 5c85a6d89d..c5ffa168e6 100644 --- a/spec/jobs/course/assessment/submission/schedule_expiring_submissions_job_spec.rb +++ b/spec/jobs/course/assessment/submission/schedule_expiring_submissions_job_spec.rb @@ -52,6 +52,9 @@ def enqueued_for(submission) context 'when a submission is past due (its scheduled job was lost)' do let(:end_at) { (10.minutes + delay).before(Time.zone.now) } + # The attempt was begun before the deadline it has since blown past. + before { submission.update_column(:created_at, 1.hour.ago) } + it 'schedules an immediate force-submit job' do run jobs = enqueued_for(submission) diff --git a/spec/models/course/assessment/submission_force_submit_spec.rb b/spec/models/course/assessment/submission_force_submit_spec.rb index e7e9912c78..d5e96dca5e 100644 --- a/spec/models/course/assessment/submission_force_submit_spec.rb +++ b/spec/models/course/assessment/submission_force_submit_spec.rb @@ -62,6 +62,34 @@ is_expected.to be_nil end end + + context 'when the submission was created after the deadline (e.g. a staff test run)' do + let(:late_allowed) { false } + let(:end_at) { 20.minutes.ago } + + it 'does not enforce the already-passed deadline' do + is_expected.to be_nil + end + + context 'but a time limit still applies' do + let(:time_limit) { 30 } + + it 'still enforces the time limit from creation' do + is_expected.to be_within(1.second).of(submission.created_at + 30.minutes) + end + end + end + + context 'when the submission was created before a since-passed deadline' do + let(:late_allowed) { false } + let(:end_at) { 20.minutes.ago } + + before { submission.update_column(:created_at, 1.hour.ago) } + + it 'still enforces the deadline (the student began in time)' do + is_expected.to be_within(1.second).of(end_at) + end + end end describe 'the unsubmit and finalise events' do @@ -90,6 +118,8 @@ describe '#force_submit_overdue?' do let(:late_allowed) { false } + # An overdue attempt was necessarily begun before its deadline. + before { submission.update_column(:created_at, 1.hour.ago) } context 'when past the force-submit time plus grace' do let(:end_at) { (10.minutes + delay).before(Time.zone.now) }