Skip to content

Commit 3d94b2d

Browse files
authored
Displayed late penalty indicator for extensions in Groups table (#7689)
Fixes #7677.
1 parent 50143bd commit 3d94b2d

File tree

8 files changed

+241
-22
lines changed

8 files changed

+241
-22
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,6 @@ config/environments/*.local.yml
8282
# local docker compose overrides
8383
docker-compose.override.yml
8484
compose.override.yaml
85+
86+
# Optional VSCode container configuration
87+
.devcontainer

Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Added new loading spinner icon for tables (#7602)
99
- Update message and page displaying cannot create new course via external LTI tool (#7669)
1010
- Provide file viewer the option to render Microsoft files (#7676)
11+
- Display late submission selection and add accompanying filter (#7689)
1112

1213
### 🐛 Bug fixes
1314
- Fixed N+1 query problem in StudentsController by eager loading user association (#7678)

app/javascript/Components/Helpers/table_helpers.jsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,27 @@ export function customNoDataComponent({children, loading}) {
262262
export function customNoDataProps({state}) {
263263
return {loading: state.loading, data: state.data};
264264
}
265+
266+
export function getTimeExtension(extension, timePeriods) {
267+
return timePeriods
268+
.map(key => {
269+
const val = extension[key];
270+
271+
if (!val) {
272+
return null;
273+
}
274+
// don't build these strings dynamically or they will be missed by the i18n-tasks checkers.
275+
if (key === "weeks") {
276+
return `${val} ${I18n.t("durations.weeks", {count: val})}`;
277+
} else if (key === "days") {
278+
return `${val} ${I18n.t("durations.days", {count: val})}`;
279+
} else if (key === "hours") {
280+
return `${val} ${I18n.t("durations.hours", {count: val})}`;
281+
} else if (key === "minutes") {
282+
return `${val} ${I18n.t("durations.minutes", {count: val})}`;
283+
}
284+
return "";
285+
})
286+
.filter(Boolean)
287+
.join(", ");
288+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import {render, screen} from "@testing-library/react";
2+
import {GroupsManager} from "../groups_manager";
3+
import {beforeEach, describe, expect, it} from "@jest/globals";
4+
import {getTimeExtension} from "../Helpers/table_helpers";
5+
6+
jest.mock("@fortawesome/react-fontawesome", () => ({
7+
FontAwesomeIcon: () => {
8+
return null;
9+
},
10+
}));
11+
12+
const groupMock = [
13+
{
14+
group_name: "c6scriab",
15+
inactive: false,
16+
instructor_approved: true,
17+
members: [
18+
{
19+
0: "c6scriab",
20+
1: "inviter",
21+
2: false,
22+
display_label: "(inviter)",
23+
},
24+
],
25+
extension: {
26+
apply_penalty: false,
27+
grouping_id: 22,
28+
id: null,
29+
note: "",
30+
},
31+
section: "",
32+
},
33+
{
34+
group_name: "group2",
35+
inactive: false,
36+
instructor_approved: true,
37+
members: [
38+
{
39+
0: "student1",
40+
1: "inviter",
41+
2: false,
42+
display_label: "(inviter)",
43+
},
44+
],
45+
section: "",
46+
extension: {
47+
apply_penalty: true,
48+
days: 2,
49+
grouping_id: 16,
50+
hours: 0,
51+
id: 51,
52+
minutes: 0,
53+
note: "",
54+
weeks: 0,
55+
},
56+
},
57+
];
58+
const studentMock = [
59+
{
60+
assigned: true,
61+
first_name: "coolStudent",
62+
hidden: false,
63+
id: 8,
64+
last_name: "Alberic",
65+
user_name: "student1",
66+
},
67+
];
68+
69+
describe("GroupsManager", () => {
70+
let filter_method = null;
71+
let wrapper = React.createRef();
72+
73+
beforeEach(async () => {
74+
fetch.mockReset();
75+
fetch.mockResolvedValueOnce({
76+
ok: true,
77+
json: jest.fn().mockResolvedValueOnce({
78+
templates: [],
79+
groups: groupMock,
80+
exam_templates: [],
81+
students: studentMock,
82+
clone_assignments: [],
83+
}),
84+
});
85+
const props = {
86+
course_id: 1,
87+
timed: false,
88+
assignment_id: 2,
89+
scanned_exam: false,
90+
examTemplates: [],
91+
times: ["weeks", "days", "hours", "minutes"],
92+
};
93+
render(<GroupsManager {...props} ref={wrapper} />);
94+
// wait for page to load and render content
95+
await screen.findByText("abcd").catch(err => err);
96+
// to view screen render: screen.debug(undefined, 300000)
97+
});
98+
99+
describe("DueDateExtensions", () => {
100+
beforeEach(() => {
101+
filter_method =
102+
wrapper.current.groupsTable.wrapped.checkboxTable.props.columns[5].filterMethod;
103+
});
104+
105+
it("append (late submissions accepted) to assignments with extensions", async () => {
106+
const groupWithExtension = groupMock[1];
107+
const timePeriods = ["weeks", "days", "hours", "minutes"];
108+
const timeExtension = getTimeExtension(groupWithExtension.extension, timePeriods);
109+
const searchTerm = `${timeExtension} (${I18n.t("groups.late_submissions_accepted")})`;
110+
expect(await screen.getByRole("link", {name: searchTerm})).toBeInTheDocument();
111+
});
112+
113+
it("returns true when the selected value is all", () => {
114+
expect(filter_method({value: "all"})).toEqual(true);
115+
});
116+
117+
describe("withExtension: false", () => {
118+
it("returns true when assignments without an extension are present", () => {
119+
const rowMock = {_original: {extension: {}}};
120+
const filterOptionsMock = JSON.stringify({withExtension: false});
121+
expect(filter_method({value: filterOptionsMock}, rowMock)).toEqual(true);
122+
});
123+
it("returns false when assignments with an extension are present", () => {
124+
const rowMock = {_original: {extension: {hours: 1}}};
125+
const filterOptionsMock = JSON.stringify({withExtension: false});
126+
expect(filter_method({value: filterOptionsMock}, rowMock)).toEqual(false);
127+
});
128+
});
129+
130+
describe("withExtension: true", () => {
131+
describe("withLateSubmission: true", () => {
132+
it("returns true when assignments have a late submission rule applied", () => {
133+
const rowMock = {_original: {extension: {hours: 1, apply_penalty: true}}};
134+
const filterOptionsMock = JSON.stringify({withExtension: true, withLateSubmission: true});
135+
expect(filter_method({value: filterOptionsMock}, rowMock)).toEqual(true);
136+
});
137+
it("returns false when assignments are missing an extension", () => {
138+
const rowMock = {_original: {extension: {apply_penalty: true}}};
139+
const filterOptionsMock = JSON.stringify({withExtension: true, withLateSubmission: true});
140+
expect(filter_method({value: filterOptionsMock}, rowMock)).toEqual(false);
141+
});
142+
});
143+
describe("withLateSubmission: false", () => {
144+
it("returns true when assignments are missing an extension", () => {
145+
const rowMock = {_original: {extension: {hours: 1, apply_penalty: true}}};
146+
const filterOptionsMock = JSON.stringify({
147+
withExtension: true,
148+
withLateSubmission: false,
149+
});
150+
expect(filter_method({value: filterOptionsMock}, rowMock)).toEqual(false);
151+
});
152+
153+
it("returns false when assignments have a late submission rule applied", () => {
154+
const rowMock = {_original: {extension: {hours: 1, apply_penalty: true}}};
155+
const filterOptionsMock = JSON.stringify({
156+
withExtension: true,
157+
withLateSubmission: false,
158+
});
159+
expect(filter_method({value: filterOptionsMock}, rowMock)).toEqual(false);
160+
});
161+
});
162+
});
163+
});
164+
});

app/javascript/Components/groups_manager.jsx

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
44

55
import {withSelection, CheckboxTable} from "./markus_with_selection_hoc";
66
import ExtensionModal from "./Modals/extension_modal";
7-
import {durationSort, selectFilter} from "./Helpers/table_helpers";
7+
import {durationSort, selectFilter, getTimeExtension} from "./Helpers/table_helpers";
88
import AutoMatchModal from "./Modals/auto_match_modal";
99
import CreateGroupModal from "./Modals/create_group_modal";
1010
import RenameGroupModal from "./Modals/rename_group_modal";
@@ -556,26 +556,12 @@ class RawGroupsTable extends React.Component {
556556
accessor: "extension",
557557
show: !this.props.scanned_exam,
558558
Cell: row => {
559-
let extension = this.props.times
560-
.map(key => {
561-
const val = row.original.extension[key];
562-
if (val) {
563-
// don't build these strings dynamically or they will be missed by the i18n-tasks checkers.
564-
if (key === "weeks") {
565-
return `${val} ${I18n.t("durations.weeks", {count: val})}`;
566-
} else if (key === "days") {
567-
return `${val} ${I18n.t("durations.days", {count: val})}`;
568-
} else if (key === "hours") {
569-
return `${val} ${I18n.t("durations.hours", {count: val})}`;
570-
} else if (key === "minutes") {
571-
return `${val} ${I18n.t("durations.minutes", {count: val})}`;
572-
} else {
573-
return "";
574-
}
575-
}
576-
})
577-
.filter(Boolean)
578-
.join(", ");
559+
const timeExtension = getTimeExtension(row.original.extension, this.props.times);
560+
const lateSubmissionText = row.original.extension.apply_penalty
561+
? `(${I18n.t("groups.late_submissions_accepted")})`
562+
: "";
563+
const extension = `${timeExtension} ${lateSubmissionText}`;
564+
579565
if (!!extension) {
580566
return (
581567
<div>
@@ -599,8 +585,39 @@ class RawGroupsTable extends React.Component {
599585
);
600586
}
601587
},
602-
filterable: false,
603588
sortMethod: durationSort,
589+
Filter: selectFilter,
590+
filterMethod: (filter, row) => {
591+
if (filter.value === "all") {
592+
return true;
593+
}
594+
const applyPenalty = row._original.extension.apply_penalty;
595+
const {withExtension, withLateSubmission} = JSON.parse(filter.value);
596+
// If there is an extension applied, the extension object will contain a property called hours
597+
const hasExtension = Object.hasOwn(row._original.extension, "hours");
598+
599+
if (!withExtension) {
600+
return !hasExtension;
601+
}
602+
if (withLateSubmission) {
603+
return hasExtension && applyPenalty;
604+
}
605+
return hasExtension && !applyPenalty;
606+
},
607+
filterOptions: [
608+
{
609+
value: JSON.stringify({withExtension: false}),
610+
text: I18n.t("groups.groups_without_extension"),
611+
},
612+
{
613+
value: JSON.stringify({withExtension: true, withLateSubmission: true}),
614+
text: I18n.t("groups.groups_with_extension.with_late_submission"),
615+
},
616+
{
617+
value: JSON.stringify({withExtension: true, withLateSubmission: false}),
618+
text: I18n.t("groups.groups_with_extension.without_late_submission"),
619+
},
620+
],
604621
},
605622
];
606623

@@ -825,3 +842,5 @@ export function makeGroupsManager(elem, props) {
825842
root.render(<GroupsManager {...props} ref={component} />);
826843
return component;
827844
}
845+
846+
export {GroupsManager};

config/i18n-tasks.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ ignore_unused:
120120
- '*.*.default'
121121
- 'exam_templates.*' # TODO: review these ones
122122
- 'notification_mailer.*'
123+
- 'durations.weeks.*' # TODO: review this
124+
- 'durations.days.*' # TODO: review this
123125
- 'durations.hours.*' # TODO: review this
124126
- 'durations.minutes.*' # TODO: review this
125127
- 'date.formats.*' # See https://github.com/glebm/i18n-tasks/issues/240

config/locales/views/groups/en.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ en:
2828
grace_day_over_limit: You have assigned one or more students who has less grace credits than the amount already used by group %{group} for this assignment.
2929
group_name_already_in_use: This name is already in use for this assignment.
3030
group_name_already_in_use_diff_assignment: The group can't be renamed, because it would result in the loss of submission data. Try a name that is not in use for another assignment.
31+
groups_with_extension:
32+
with_late_submission: Groups with Extension (Late Submissions Accepted)
33+
without_late_submission: Groups with Extension (Late Submissions Not Accepted)
34+
groups_without_extension: Groups Without Extension
3135
help: Manage student groups. You can create new groups and manually add and remove students to groups.
3236
invalid_group_warning: 'Your group is currently invalid. You probably haven''t met the group size minimum. You may not be able to submit anything, and your work may not be graded, until you have met this minimum. '
3337
invalidate_confirm: This will prevent this group from submitting, even with the required number of students. Are you sure?
@@ -45,6 +49,7 @@ en:
4549
success: Invitation(s) successful.
4650
is_not_valid: Group is not valid
4751
is_valid: Group is valid
52+
late_submissions_accepted: late submissions accepted
4853
manage_groups: Manage Groups
4954
members:
5055
cancel_invitation: Cancel invitation

doc/markus-contributors.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ Marc Palermo
138138
Mark Kazakevich
139139
Mark Rada
140140
Maryna Moskalenko
141+
Mateo Naranjo
141142
Matthew Austin
142143
Mélanie Gaudet
143144
Melissa Neubert

0 commit comments

Comments
 (0)