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
2 changes: 1 addition & 1 deletion website/src/views/timetable/ExamCalendar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
.day,
.dayName,
.dayDate {
width: 16.6666%;
width: 14.2857%;
padding: 0 0.25rem;
border: 1px solid var(--gray-lighter);
border-width: 0 1px 0 0;
Expand Down
64 changes: 59 additions & 5 deletions website/src/views/timetable/ExamCalendar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { ModuleWithColor } from 'types/views';
import mockModules from '__mocks__/modules';
/** @vars {Module} */
import GER1000 from '__mocks__/modules/GER1000.json';
/** @vars {Module} */
import PC1222 from '__mocks__/modules/PC1222.json';
/** @vars {Module} */
import CS1010S from '__mocks__/modules/CS1010S.json';
/** @vars {Module} */
import ACC2002 from '__mocks__/modules/ACC2002.json';
import { Semester } from 'types/modules';

import ExamCalendar, { getTimeSegment } from './ExamCalendar';
Expand Down Expand Up @@ -43,12 +49,60 @@ function make(modules: ModuleWithColor[] = [], semester: Semester = 1) {
// - PC1222: 2017-12-05 (Tue) Evening
// - CS3216: No exams
describe(ExamCalendar, () => {
test('only show Saturday if there is a Saturday exam', () => {
const withSaturdayExams = make([GER1000 as unknown as ModuleWithColor]);
const withoutSaturdayExams = make(modulesWithColor);
test('show the full week (Mon-Sun) when there is a Saturday exam', () => {
// GER1000 has a Saturday exam (2017-11-25), so the weekend should be shown
const wrapper = make([GER1000 as unknown as ModuleWithColor]);

expect(wrapper.find('thead th')).toHaveLength(7);
expect(wrapper.find('thead th').map((th) => th.text())).toEqual([
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
'Sun',
]);
});

test('show only weekdays (Mon-Fri) when there is no Saturday exam', () => {
const wrapper = make(modulesWithColor);

expect(wrapper.find('thead th')).toHaveLength(5);
expect(wrapper.find('thead th').map((th) => th.text())).toEqual([
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
]);
});

test('show only one week when all exams fall within the same week', () => {
// CS1010S (2017-11-29, Wed) and ACC2002 (2017-12-01, Fri) both fall in the week of Mon
// 2017-11-27, so only that single week should be rendered.
const wrapper = make([
CS1010S as unknown as ModuleWithColor,
ACC2002 as unknown as ModuleWithColor,
]);

expect(wrapper.find('tbody tr')).toHaveLength(TR_PER_WEEK);
expect(wrapper.find(Link)).toHaveLength(2);
expect(wrapper.find(`.${styles.dayDate} time`).first().text()).toEqual('Nov 27');
});

test('show only the exam week when a single module has an exam', () => {
const wrapper = make([PC1222 as unknown as ModuleWithColor]);

expect(wrapper.find('tbody tr')).toHaveLength(TR_PER_WEEK);
expect(wrapper.find(`.${styles.dayDate} time`).first().text()).toEqual('Dec 4');
});

test('show a message instead of a table when there are no exams', () => {
const wrapper = make([]);

expect(withSaturdayExams.find('thead th')).toHaveLength(6);
expect(withoutSaturdayExams.find('thead th')).toHaveLength(5);
expect(wrapper.find(`.${styles.noExams}`)).toHaveLength(1);
expect(wrapper.find('table')).toHaveLength(0);
});

test('show month names only in the first cell and on first weekday of month', () => {
Expand Down
66 changes: 29 additions & 37 deletions website/src/views/timetable/ExamCalendar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { PureComponent } from 'react';
import NUSModerator from 'nusmoderator';
import { groupBy, range } from 'lodash-es';
import classnames from 'classnames';
import { addDays } from 'date-fns';
import { differenceInCalendarWeeks, startOfWeek } from 'date-fns';

import { Semester, WorkingDays } from 'types/modules';
import { Semester, DaysOfWeek } from 'types/modules';
import { ModuleWithColor, ModuleWithExamTime, TimeSegment } from 'types/views';
import config from 'config';
import { formatExamDate, getExamDate } from 'utils/modules';
import { toSingaporeTime } from 'utils/timify';
import elements from 'views/elements';
Expand Down Expand Up @@ -40,32 +38,24 @@ export default class ExamCalendar extends PureComponent<Props> {
// Utility function to get the first day of exams and calculate the number of weeks
getExamCalendar(): [Date, number] {
const { semester } = this.props;
const year = `${config.academicYear.slice(2, 4)}/${config.academicYear.slice(-2)}`;
let firstDayOfExams = NUSModerator.academicCalendar.getExamWeek(year, semester);
firstDayOfExams = new Date(
firstDayOfExams.getTime() - firstDayOfExams.getTimezoneOffset() * 60 * 1000,
);

let weekCount = 0;
let lastDayOfExams = addDays(firstDayOfExams, 0);
const examDates = this.getVisibleModules()
.map((module) => getExamDate(module, semester))
.filter(Boolean as unknown as (dateString: string | null) => dateString is string)
.map((dateString) => toSingaporeTime(dateString));

// Check modules for outliers, eg. GER1000 that has exams on the Saturday before the exam week
// and expand the range accordingly
this.getVisibleModules().forEach((module) => {
const dateString = getExamDate(module, semester);
if (!dateString) return;

const date = toSingaporeTime(dateString);
while (date < firstDayOfExams) {
firstDayOfExams = addDays(firstDayOfExams, -7);
weekCount += 1;
}

while (date > lastDayOfExams) {
lastDayOfExams = addDays(lastDayOfExams, 7);
weekCount += 1;
}
});
if (examDates.length === 0) {
return [new Date(), 0];
}

const firstExamDate = examDates.reduce((a, b) => (a < b ? a : b));
const lastExamDate = examDates.reduce((a, b) => (a > b ? a : b));

const firstDayOfExams = startOfWeek(firstExamDate, { weekStartsOn: 1 });
const weekCount =
differenceInCalendarWeeks(lastExamDate, firstExamDate, {
weekStartsOn: 1,
}) + 1;

return [firstDayOfExams, weekCount];
}
Expand Down Expand Up @@ -111,12 +101,14 @@ export default class ExamCalendar extends PureComponent<Props> {

const modulesWithExams = this.modulesWithExamDate();

// Get the number of days of the week which have exams on them. Default to Monday to Friday
// (5 days), and expand as necessary
const daysWithExams = Math.max(
5,
...modulesWithExams.map((module) => toSingaporeTime(module.dateTime).getDay()),
);
const minDisplayDays = 5;
const maxDisplayDays = 7;

const daysToDisplay = modulesWithExams
.map((module) => toSingaporeTime(module.dateTime).getDay())
.some((day) => day === 6)
? maxDisplayDays
: minDisplayDays;

const modulesByExamDate = groupBy(modulesWithExams, (module) => module.date);

Expand All @@ -132,9 +124,9 @@ export default class ExamCalendar extends PureComponent<Props> {
<table>
<thead>
<tr>
{range(daysWithExams).map((day) => (
{range(daysToDisplay).map((day) => (
<th key={day} className={styles.dayName}>
{WorkingDays[day].slice(0, 3)}
{DaysOfWeek[day].slice(0, 3)}
</th>
))}
</tr>
Expand All @@ -144,7 +136,7 @@ export default class ExamCalendar extends PureComponent<Props> {
{range(weekCount).map((week) => (
<ExamWeek
key={week}
days={daysWithExams}
days={daysToDisplay}
weekNumber={week}
firstDayOfExams={firstDayOfExams}
modules={modulesByExamDate}
Expand Down
12 changes: 10 additions & 2 deletions website/src/views/timetable/ExamWeek.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';

import ExamWeek from './ExamWeek';
import { toSingaporeTime } from 'utils/timify';

function make(props = {}) {
const propsWithDefaults = {
Expand Down Expand Up @@ -30,7 +31,9 @@ describe(ExamWeek, () => {
});

test('show month name when the months changes', () => {
const weekOfApril29 = make({ firstDayOfExams: new Date('2019-04-29T00:00:00Z') });
const weekOfApril29 = make({
firstDayOfExams: toSingaporeTime('2019-04-29T00:00:00Z'),
});
expect(weekOfApril29.find('th time').map((ele) => ele.text())).toEqual([
'Apr 29',
'30',
Expand All @@ -41,11 +44,16 @@ describe(ExamWeek, () => {

const weekOfDec3 = make({
weekNumber: 1,
firstDayOfExams: new Date(new Date('2018-11-26T00:00:00Z')),
firstDayOfExams: toSingaporeTime('2018-11-26T00:00:00Z'),
});
expect(weekOfDec3.find('th time').first().text()).toEqual('Dec 3');
});

test('render dates using local calendar time', () => {
const wrapper = make({ firstDayOfExams: toSingaporeTime('2020-05-03T16:00:00Z') });
expect(wrapper.find('th time').first().text()).toEqual('May 4');
});
Comment thread
leslieyip02 marked this conversation as resolved.

test('highlight today', () => {
const weekOfToday = make();
expect(weekOfToday.find('th span').first().text()).toEqual('Today');
Expand Down
4 changes: 2 additions & 2 deletions website/src/views/timetable/ExamWeek.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ const ExamWeekComponent: React.FC<Props> = (props) => {
<tr className={styles.timeRow}>
{dayDates.map((date) => {
// Show the month name when the month changes on the calendar
let examDateString = String(date.getUTCDate());
let examDateString = String(date.getDate());
if (currentMonth !== date.getMonth()) {
examDateString = `${MONTHS[date.getUTCMonth()]} ${examDateString}`;
examDateString = `${MONTHS[date.getMonth()]} ${examDateString}`;
currentMonth = date.getMonth();
}
return (
Expand Down