Skip to content

Commit c7a7f43

Browse files
authored
Merge pull request #1375 from isaacphysics/improvement/autofill-assignment-due
Improvement/autofill assignment due
2 parents 4281a44 + c93668e commit c7a7f43

File tree

10 files changed

+259
-112
lines changed

10 files changed

+259
-112
lines changed

config/jest/jest.config.common.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module.exports = {
44
"!src/**/*.d.ts"
55
],
66
"resolver": "jest-pnp-resolver",
7+
globalSetup: "<rootDir>/src/test/globalSetup.ts",
78
setupFiles: [
89
"<rootDir>/config/jest/jest.polyfills.js"
910
],

src/app/components/elements/inputs/DateInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,6 @@ export const DateInput = (props: DateInputProps) => {
260260
</div>
261261
{(props.noClear === undefined || !props.noClear) && <Button close {...controlPropsWithValidationStripped} className="mx-1" aria-label={`Clear date${props.labelSuffix ? props.labelSuffix : ""}`} onClick={clear} />}
262262
</InputGroup>
263-
<Input innerRef={hiddenRef} type="hidden" name={props.name} value={calculateHiddenValue()} {...controlProps} />
263+
<Input data-testid='date-input' innerRef={hiddenRef} type="hidden" name={props.name} value={calculateHiddenValue()} {...controlProps} />
264264
</React.Fragment>;
265265
};

src/app/components/pages/AssignmentSchedule.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ import {
5353
TAG_LEVEL,
5454
tags,
5555
TODAY,
56-
useDeviceSize
56+
useDeviceSize,
57+
UTC_MIDNIGHT_IN_SIX_DAYS
5758
} from "../../services";
5859
import {
5960
AppGroup,
@@ -415,7 +416,7 @@ const AssignmentModal = ({user, showSetAssignmentUI, toggleSetAssignmentUI, assi
415416
// Create from scratch
416417
setSelectedGameboard(undefined);
417418
setScheduledStartDate(undefined);
418-
setDueDate(undefined);
419+
setDueDate(UTC_MIDNIGHT_IN_SIX_DAYS);
419420
setAssignmentNotes(undefined);
420421
}
421422
}, [assignmentToCopy]);
@@ -432,8 +433,8 @@ const AssignmentModal = ({user, showSetAssignmentUI, toggleSetAssignmentUI, assi
432433
})).then((result) => {
433434
if (assignGameboard.fulfilled.match(result)) {
434435
setSelectedGroups([]);
435-
setDueDate(undefined);
436436
setScheduledStartDate(undefined);
437+
setDueDate(UTC_MIDNIGHT_IN_SIX_DAYS);
437438
setAssignmentNotes('');
438439
}
439440
// Fails silently if assignGameboard throws an error - we let it handle opening toasts for errors
@@ -506,11 +507,9 @@ const AssignmentModal = ({user, showSetAssignmentUI, toggleSetAssignmentUI, assi
506507
<DateInput value={dueDate} placeholder="Select your due date..." yearRange={yearRange}
507508
onChange={(e: ChangeEvent<HTMLInputElement>) => setDueDate(e.target.valueAsDate as Date)}
508509
/>
510+
{!dueDate && <small className={"pt-2 text-danger"}>Since January 2025, due dates are required for assignments.</small>}
509511
{dueDateInvalid && <small className={"pt-2 text-danger"}>Due date must be on or after start date.</small>}
510512
</Label>
511-
<Alert color={siteSpecific("warning", "info")} className="py-1">
512-
Since January 2025, due dates are required for assignments.
513-
</Alert>
514513
{isStaff(user) && <Label className="w-100 pb-2">Notes (optional):
515514
<Input type="textarea"
516515
spellCheck={true}

src/app/components/pages/SetAssignments.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ import {
5858
siteSpecific,
5959
useDeviceSize,
6060
useGameboards,
61-
TODAY
61+
TODAY,
62+
UTC_MIDNIGHT_IN_SIX_DAYS
6263
} from "../../services";
6364
import {IsaacSpinner, Loading} from "../handlers/IsaacSpinner";
6465
import {GameboardDTO, RegisteredUserDTO, UserGroupDTO} from "../../../IsaacApiTypes";
@@ -74,9 +75,10 @@ interface AssignGroupProps {
7475
groups: UserGroupDTO[];
7576
board: GameboardDTO | undefined;
7677
}
78+
7779
const AssignGroup = ({groups, board}: AssignGroupProps) => {
7880
const [selectedGroups, setSelectedGroups] = useState<Item<number>[]>([]);
79-
const [dueDate, setDueDate] = useState<Date>();
81+
const [dueDate, setDueDate] = useState<Date | undefined>(UTC_MIDNIGHT_IN_SIX_DAYS);
8082
const [scheduledStartDate, setScheduledStartDate] = useState<Date>();
8183
const [assignmentNotes, setAssignmentNotes] = useState<string>();
8284
const user = useAppSelector(selectors.user.loggedInOrNull);
@@ -88,7 +90,7 @@ const AssignGroup = ({groups, board}: AssignGroupProps) => {
8890
dispatch(assignGameboard({boardId: board?.id as string, groups: selectedGroups, dueDate, scheduledStartDate, notes: assignmentNotes, userId: user?.id})).then(success => {
8991
if (success) {
9092
setSelectedGroups([]);
91-
setDueDate(undefined);
93+
setDueDate(UTC_MIDNIGHT_IN_SIX_DAYS);
9294
setScheduledStartDate(undefined);
9395
setAssignmentNotes('');
9496
}
@@ -111,7 +113,7 @@ const AssignGroup = ({groups, board}: AssignGroupProps) => {
111113
}
112114

113115
return <Container className="py-2">
114-
<Label className="w-100 pb-2">Group(s):
116+
<Label data-testid="modal-groups-selector" className="w-100 pb-2">Group(s):
115117
<StyledSelect inputId="groups-to-assign" isMulti isClearable placeholder="None"
116118
value={selectedGroups}
117119
closeMenuOnSelect={false}
@@ -127,12 +129,9 @@ const AssignGroup = ({groups, board}: AssignGroupProps) => {
127129
<Label className="w-100 pb-2">Due date reminder
128130
<DateInput value={dueDate} placeholder="Select your due date..." yearRange={yearRange}
129131
onChange={(e: ChangeEvent<HTMLInputElement>) => setDueDate(e.target.valueAsDate as Date)} /> {/* DANGER here with force-casting Date|null to Date */}
132+
{!dueDate && <small className={"pt-2 text-danger"}>Since {siteSpecific("Jan", "January")} 2025, due dates are required for assignments.</small>}
130133
{dueDateInvalid && <small className={"pt-2 text-danger"}>Due date must be on or after start date and in the future.</small>}
131-
{dueDateInvalid && startDateInvalid && <br/>}
132134
</Label>
133-
<Alert color={siteSpecific("warning", "info")} className="py-1 px-2">
134-
Since {siteSpecific("Jan", "January")} 2025, due dates are required for assignments.
135-
</Alert>
136135
{isEventLeaderOrStaff(user) && <Label className="w-100 pb-2">Notes (optional):
137136
<Input type="textarea"
138137
spellCheck={true}
@@ -180,10 +179,8 @@ const SetAssignmentsModal = (props: SetAssignmentsModalProps) => {
180179
}
181180
}
182181

183-
const description = siteSpecific(
184-
"Manage assignment of groups to the selected gameboard",
185-
"Select a group to which to assign the quiz"
186-
);
182+
const description = "Scheduled assignments appear to students on the morning of the day chosen, otherwise assignments appear immediately. " +
183+
"Assignments are due by the end of the day indicated.";
187184

188185
return <Modal isOpen={isOpen} data-testid={"set-assignment-modal"} toggle={toggle}>
189186
<ModalHeader data-testid={"modal-header"} role={"heading"} className={"text-break d-flex justify-content-between"} close={
@@ -212,7 +209,7 @@ const SetAssignmentsModal = (props: SetAssignmentsModalProps) => {
212209
<div className="py-2">
213210
<Label>Pending {siteSpecific("assignments", "quiz assignments")}: <span className="icon-help mx-1" id={`pending-assignments-help-${board?.id}`}/></Label>
214211
<UncontrolledTooltip placement="left" autohide={false} target={`pending-assignments-help-${board?.id}`}>
215-
{siteSpecific("Assignments", "Quizzes")} that are scheduled to begin at a future date. Once the start date passes, students
212+
These {siteSpecific("assignments", "quizzes")} are scheduled to begin at a future date. On the morning of the scheduled date, students
216213
will be able to see the {siteSpecific("assignment", "quiz")}, and will receive a notification email.
217214
</UncontrolledTooltip>
218215
{scheduledAssignees.length > 0

src/app/services/dates.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,24 @@ export const nthHourOf = (n: number, d: Date | number) => {
55
};
66

77
export const TODAY = () => {
8-
return nthHourOf(0, new Date());
8+
return nthHourOf(0, new Date(Date.now()));
9+
};
10+
11+
export const UTC_MIDNIGHT_IN_SIX_DAYS = () => {
12+
return addDays(6, nthUtcHourOf(0, new Date(Date.now())));
913
};
1014

1115
export const MONTH_NAMES = [ "January", "February", "March", "April", "May", "June",
1216
"July", "August", "September", "October", "November", "December" ];
17+
18+
export const nthUtcHourOf = (n: number, d: Date | number) => {
19+
const newDate = new Date(d.valueOf());
20+
newDate.setUTCHours(n, 0, 0, 0);
21+
return newDate;
22+
};
23+
24+
const addDays = (days: number, date: Date, ) => {
25+
const newDate = new Date(date.valueOf());
26+
newDate.setDate(date.getDate() + days);
27+
return newDate;
28+
};

src/mocks/handlers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,9 @@ export const buildFunctionHandler = <T extends string, V extends JsonBodyType>(p
255255
return HttpResponse.json(fn(params), { status: 200, });
256256
});
257257
};
258+
export const buildPostHandler = <T, V extends JsonBodyType>(path: string, fn: (p: T) => V) => {
259+
return http.post(API_PATH + path, async ({ request }) => {
260+
const body = await request.json() as T;
261+
return HttpResponse.json(fn(body), { status: 200, });
262+
});
263+
};

src/test/environment.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
describe('Testing environment', () => {
2+
it('is set to the Europe/London timezone', () => {
3+
expect(new Date("2025-04-01").getTimezoneOffset()).toEqual(-60);
4+
});
5+
});

src/test/globalSetup.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = () => {
2+
process.env.TZ = 'Europe/London';
3+
};

0 commit comments

Comments
 (0)