Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions app/controllers/submissions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
206 changes: 205 additions & 1 deletion app/javascript/Components/__tests__/submission_file_manager.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<img src="" /><a href=""> HelloWorld.java</a>',
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 = `<div id="content"></div>`;
render(
<SubmissionFileManager
course_id={1}
assignment_id={1}
starterFileChanged={false}
{...props}
/>
);
};

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();
});
});
});
33 changes: 33 additions & 0 deletions app/javascript/Components/submission_file_manager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"))
Expand Down
2 changes: 1 addition & 1 deletion app/models/penalty_decay_period_submission_rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion app/models/penalty_period_submission_rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions app/views/submissions/file_manager.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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 %>,
Expand Down
3 changes: 3 additions & 0 deletions config/locales/models/submission_rules/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
Loading