diff --git a/Changelog.md b/Changelog.md
index 70bb332e83..3df37f03a7 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -7,6 +7,7 @@
### 🚨 Breaking changes
### ✨ New features and improvements
+- Added a confirm dialog when a student tries to submit work after the deadline has passed (#8003)
- Added a confirm dialog to the Upload Scans form that appears when no template divisions are assigned to the selected exam template (#7993)
- Migrated `MarkingSchemesTable` component to React Table V8 (#7985)
- Removed Graders Subcomponent and added a Graders column in the Assignment Grades tab (#7967)
diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb
index e2e6f5e39c..17de5b3e81 100644
--- a/app/controllers/submissions_controller.rb
+++ b/app/controllers/submissions_controller.rb
@@ -135,6 +135,10 @@ def file_manager
set_filebrowser_vars(@grouping)
flash_file_manager_messages
+ past_due_date = @assignment.grouping_past_due_date?(@grouping)
+ past_collection_date = @grouping.past_collection_date?
+ @show_late_submit_confirmation = past_due_date && !past_collection_date
+
render 'file_manager', layout: 'assignment_content', locals: {}
end
diff --git a/app/javascript/Components/__tests__/submission_file_manager.test.jsx b/app/javascript/Components/__tests__/submission_file_manager.test.jsx
index 53b987774b..35af318506 100644
--- a/app/javascript/Components/__tests__/submission_file_manager.test.jsx
+++ b/app/javascript/Components/__tests__/submission_file_manager.test.jsx
@@ -205,7 +205,211 @@ describe("For the SubmissionFileManager", () => {
]);
await userEvent.click(screen.getByRole("button", {name: I18n.t("save"), hidden: true}));
- expect(screen.findByRole("progressbar", {hidden: true})).rejects.toThrow();
+ await expect(screen.findByRole("progressbar", {hidden: true})).rejects.toThrow();
+ });
+ });
+});
+
+describe("For the late submit confirm dialog", () => {
+ const LATE_SUBMIT_MESSAGES = {
+ GracePeriodSubmissionRule: I18n.t(
+ "activerecord.attributes.grace_period_submission_rule.upload_late_confirmation_dialog"
+ ),
+ PenaltyDecayPeriodSubmissionRule: I18n.t(
+ "activerecord.attributes.penalty_decay_period_submission_rule.upload_late_confirmation_dialog"
+ ),
+ PenaltyPeriodSubmissionRule: I18n.t(
+ "activerecord.attributes.penalty_period_submission_rule.upload_late_confirmation_dialog"
+ ),
+ };
+
+ const files_sample = {
+ entries: [
+ {
+ id: 136680,
+ url: "test.url",
+ filename: '
HelloWorld.java',
+ raw_name: "HelloWorld.java",
+ last_revised_date: "Saturday, May 14, 2022, 09:15:24 PM EDT",
+ last_modified_revision: "58ca2e15254aa63c4d41cb5db7dfc398b6bda3fb",
+ revision_by: "c5anthei",
+ submitted_date: "Saturday, May 14, 2022, 09:15:24 PM EDT",
+ type: "java",
+ key: "HelloWorld.java",
+ modified: 1652577324,
+ relativeKey: "HelloWorld.java",
+ },
+ ],
+ only_required_files: false,
+ required_files: [],
+ max_file_size: 10,
+ number_of_missing_files: 0,
+ };
+
+ const file = new File(["content"], "test.txt", {type: "text/plain"});
+ let confirmSpy;
+
+ const mockPost = () => {
+ $.post = jest.fn().mockReturnValue({
+ then: jest.fn().mockReturnThis(),
+ fail: jest.fn().mockReturnThis(),
+ always: jest.fn().mockReturnThis(),
+ });
+ };
+
+ const renderManager = (props = {}) => {
+ fetch.mockResponseOnce(JSON.stringify(files_sample));
+ document.body.innerHTML = `
`;
+ render(
+
+ );
+ };
+
+ const submitFileThroughModal = async () => {
+ const submitLink = screen.getByText(I18n.t("submit_the", {item: I18n.t("file")}));
+ await userEvent.click(submitLink);
+ await userEvent.upload(screen.getByTitle(I18n.t("modals.file_upload.file_input_label")), [
+ file,
+ ]);
+ await userEvent.click(screen.getByRole("button", {name: I18n.t("save"), hidden: true}));
+ };
+
+ const submitUrlThroughModal = async () => {
+ const submitLink = screen.getByText(
+ I18n.t("submit_the", {item: I18n.t("submissions.student.link")})
+ );
+ await userEvent.click(submitLink);
+ await userEvent.type(
+ document.querySelector('input[name="new_url"]'),
+ "https://example.com/page"
+ );
+ await userEvent.type(document.querySelector('input[name="new_url_text"]'), "example");
+ await userEvent.click(screen.getByRole("button", {name: I18n.t("save"), hidden: true}));
+ };
+
+ beforeEach(() => {
+ fetch.resetMocks();
+ confirmSpy = jest.spyOn(window, "confirm").mockReturnValue(true);
+ mockPost();
+ });
+
+ afterEach(() => {
+ confirmSpy.mockRestore();
+ });
+
+ describe("For the submission file upload modal", () => {
+ describe.each([
+ ["GracePeriodSubmissionRule"],
+ ["PenaltyDecayPeriodSubmissionRule"],
+ ["PenaltyPeriodSubmissionRule"],
+ ])("when submission_rule is %s", submissionRule => {
+ it("calls confirm with the correct message", async () => {
+ renderManager({
+ show_late_submit_confirmation: true,
+ submission_rule: submissionRule,
+ });
+ await screen.findByText("HelloWorld.java");
+ await submitFileThroughModal();
+ expect(confirmSpy).toHaveBeenCalledWith(LATE_SUBMIT_MESSAGES[submissionRule]);
+ });
+
+ it("does not upload when user cancels the confirm dialog", async () => {
+ confirmSpy.mockReturnValue(false);
+ renderManager({
+ show_late_submit_confirmation: true,
+ submission_rule: submissionRule,
+ });
+ await screen.findByText("HelloWorld.java");
+ await submitFileThroughModal();
+ expect(confirmSpy).toHaveBeenCalledWith(LATE_SUBMIT_MESSAGES[submissionRule]);
+ expect($.post).not.toHaveBeenCalled();
+ });
+
+ it("uploads when the user confirms the dialog", async () => {
+ confirmSpy.mockReturnValue(true);
+ renderManager({
+ show_late_submit_confirmation: true,
+ submission_rule: submissionRule,
+ });
+ await screen.findByText("HelloWorld.java");
+ await submitFileThroughModal();
+ expect(confirmSpy).toHaveBeenCalledWith(LATE_SUBMIT_MESSAGES[submissionRule]);
+ expect($.post).toHaveBeenCalled();
+ });
+ });
+
+ it("does not call confirm when show_late_submit_confirmation is false", async () => {
+ renderManager({show_late_submit_confirmation: false});
+ await screen.findByText("HelloWorld.java");
+ await submitFileThroughModal();
+ expect(confirmSpy).not.toHaveBeenCalled();
+ expect($.post).toHaveBeenCalled();
+ });
+
+ it("does not call confirm when show_late_submit_confirmation is true but submission_rule is missing", async () => {
+ renderManager({show_late_submit_confirmation: true});
+ await screen.findByText("HelloWorld.java");
+ await submitFileThroughModal();
+ expect(confirmSpy).not.toHaveBeenCalled();
+ expect($.post).toHaveBeenCalled();
+ });
+ });
+
+ describe("For the submit URL upload modal", () => {
+ describe.each([
+ ["GracePeriodSubmissionRule"],
+ ["PenaltyDecayPeriodSubmissionRule"],
+ ["PenaltyPeriodSubmissionRule"],
+ ])("when submission_rule is %s", submissionRule => {
+ it("calls confirm with the correct message", async () => {
+ renderManager({
+ show_late_submit_confirmation: true,
+ submission_rule: submissionRule,
+ enableUrlSubmit: true,
+ });
+ await screen.findByText("HelloWorld.java");
+ await submitUrlThroughModal();
+ expect(confirmSpy).toHaveBeenCalledWith(LATE_SUBMIT_MESSAGES[submissionRule]);
+ });
+
+ it("does not upload when user cancels the confirm dialog", async () => {
+ confirmSpy.mockReturnValue(false);
+ renderManager({
+ show_late_submit_confirmation: true,
+ submission_rule: submissionRule,
+ enableUrlSubmit: true,
+ });
+ await screen.findByText("HelloWorld.java");
+ await submitUrlThroughModal();
+ expect(confirmSpy).toHaveBeenCalledWith(LATE_SUBMIT_MESSAGES[submissionRule]);
+ expect($.post).not.toHaveBeenCalled();
+ });
+
+ it("uploads when the user confirms the dialog", async () => {
+ confirmSpy.mockReturnValue(true);
+ renderManager({
+ show_late_submit_confirmation: true,
+ submission_rule: submissionRule,
+ enableUrlSubmit: true,
+ });
+ await screen.findByText("HelloWorld.java");
+ await submitUrlThroughModal();
+ expect(confirmSpy).toHaveBeenCalledWith(LATE_SUBMIT_MESSAGES[submissionRule]);
+ expect($.post).toHaveBeenCalled();
+ });
+ });
+
+ it("does not call confirm when show_late_submit_confirmation is false", async () => {
+ renderManager({show_late_submit_confirmation: false, enableUrlSubmit: true});
+ await screen.findByText("HelloWorld.java");
+ await submitUrlThroughModal();
+ expect(confirmSpy).not.toHaveBeenCalled();
+ expect($.post).toHaveBeenCalled();
});
});
});
diff --git a/app/javascript/Components/submission_file_manager.jsx b/app/javascript/Components/submission_file_manager.jsx
index f8bb4227d4..1619395d33 100644
--- a/app/javascript/Components/submission_file_manager.jsx
+++ b/app/javascript/Components/submission_file_manager.jsx
@@ -77,7 +77,36 @@ class SubmissionFileManager extends React.Component {
}
}
+ confirmWhenLate = () => {
+ if (this.props.show_late_submit_confirmation) {
+ switch (this.props.submission_rule) {
+ case "GracePeriodSubmissionRule":
+ return confirm(
+ I18n.t(
+ "activerecord.attributes.grace_period_submission_rule.upload_late_confirmation_dialog"
+ )
+ );
+ case "PenaltyDecayPeriodSubmissionRule":
+ return confirm(
+ I18n.t(
+ "activerecord.attributes.penalty_decay_period_submission_rule.upload_late_confirmation_dialog"
+ )
+ );
+ case "PenaltyPeriodSubmissionRule":
+ return confirm(
+ I18n.t(
+ "activerecord.attributes.penalty_period_submission_rule.upload_late_confirmation_dialog"
+ )
+ );
+ }
+ }
+ return true;
+ };
+
handleCreateUrl = (url, url_text) => {
+ if (!this.confirmWhenLate()) {
+ return;
+ }
this.setState({showURLModal: false});
const data_to_upload = {
new_url: url,
@@ -99,6 +128,10 @@ class SubmissionFileManager extends React.Component {
};
handleCreateFiles = (files, path, unzip, renameTo = "") => {
+ if (!this.confirmWhenLate()) {
+ return;
+ }
+
if (
!this.props.starterFileChanged ||
confirm(I18n.t("assignments.starter_file.upload_confirmation"))
diff --git a/app/models/penalty_decay_period_submission_rule.rb b/app/models/penalty_decay_period_submission_rule.rb
index fec3837405..92e4f146ed 100644
--- a/app/models/penalty_decay_period_submission_rule.rb
+++ b/app/models/penalty_decay_period_submission_rule.rb
@@ -16,7 +16,7 @@
#
# rubocop:enable Layout/LineLength, Lint/RedundantCopDisableDirective
class PenaltyDecayPeriodSubmissionRule < SubmissionRule
- # This message will be dislayed to Students on viewing their file manager
+ # This message will be displayed to Students on viewing their file manager
# after the due date has passed, but before the calculated collection date.
validates :penalty_type,
presence: true,
diff --git a/app/models/penalty_period_submission_rule.rb b/app/models/penalty_period_submission_rule.rb
index 64b3a429b2..2cce57bdc1 100644
--- a/app/models/penalty_period_submission_rule.rb
+++ b/app/models/penalty_period_submission_rule.rb
@@ -16,7 +16,7 @@
#
# rubocop:enable Layout/LineLength, Lint/RedundantCopDisableDirective
class PenaltyPeriodSubmissionRule < SubmissionRule
- # This message will be dislayed to Students on viewing their file manager
+ # This message will be displayed to Students on viewing their file manager
# after the due date has passed, but before the calculated collection date.
validates :penalty_type,
presence: true,
diff --git a/app/views/submissions/file_manager.html.erb b/app/views/submissions/file_manager.html.erb
index 593057ace2..f7cef8ce3a 100644
--- a/app/views/submissions/file_manager.html.erb
+++ b/app/views/submissions/file_manager.html.erb
@@ -8,6 +8,8 @@
course_id: <%= @current_course.id %>,
assignment_id: <%= @assignment.id %>,
grouping_id: <%= @grouping.id %>,
+ show_late_submit_confirmation: <%= @show_late_submit_confirmation %>,
+ submission_rule: "<%= @assignment.submission_rule.type %>",
readOnly: <%= !@assignment.allow_web_submits %>,
enableSubdirs: <%= allowed_to? :manage_subdirectories? %>,
enableUrlSubmit: <%= @grouping.assignment.url_submit %>,
diff --git a/config/locales/models/submission_rules/en.yml b/config/locales/models/submission_rules/en.yml
index e95a72abd0..83dd7330d5 100644
--- a/config/locales/models/submission_rules/en.yml
+++ b/config/locales/models/submission_rules/en.yml
@@ -7,6 +7,7 @@ en:
commit_after_collection_message: The due date for this assignment, plus the maximum grace period, has passed. Your changes have been recorded, but will not be included in the grading.
description: You may submit up to a set time past the due date, provided you have enough remaining grace credits to do so.
form_description: Automatically deduct grace credits
+ upload_late_confirmation_dialog: The due date for this assignment has passed. If you submit files now, you will use grace credits, or your submission may not be graded, depending on when you submit and how many grace credits you have remaining. This action cannot be undone. Are you sure you would like to proceed?
no_late_submission_rule:
after_collection_message: The due date for this assignment has passed.
commit_after_collection_message: The due date for this assignment has passed. Your changes have been recorded, but will not be included in the grading.
@@ -17,11 +18,13 @@ en:
commit_after_collection_message: The due date for this assignment, plus the maximum late penalty period, has passed. Your changes have been recorded, but will not be included in the grading.
description: You are able to submit up to a set time past the due date, but with the appropriate percentage deducted from your final grade.
form_description: Use penalty decay formula
+ upload_late_confirmation_dialog: The due date for this assignment has passed. If you submit files now, you will incur a late penalty, or your submission may not be graded, depending on when you submit. This action cannot be undone. Are you sure you would like to proceed?
penalty_period_submission_rule:
after_collection_message: The maximum late penalty period has passed for this assignment.
commit_after_collection_message: The due date for this assignment, plus the maximum late penalty period, has passed. Your changes have been recorded, but will not be included in the grading.
description: You are able to submit up to a set time past the due date, but with the appropriate percentage deducted from your final grade.
form_description: Set manual penalty periods
+ upload_late_confirmation_dialog: The due date for this assignment has passed. If you submit files now, you will incur a late penalty, or your submission may not be graded, depending on when you submit. This action cannot be undone. Are you sure you would like to proceed?
models:
submission_rule:
one: Late Submission Policy