Skip to content

Commit c3107ad

Browse files
authored
Restore prerequisite tree on frontend (#3463)
* Enable new prereq tree * Update test snapshots * Fix mild text presentation bug * Fix lint * Remove unnecessary comment * Support nOf operator * Fix planner prereq checks * Fix planner conflict text
1 parent 9f2d45c commit c3107ad

File tree

5 files changed

+124
-23
lines changed

5 files changed

+124
-23
lines changed

website/src/types/modules.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ export type WeekRange = {
2727
};
2828

2929
// Recursive tree of module codes and boolean operators for the prereq tree
30-
export type PrereqTree = string | { and: PrereqTree[] } | { or: PrereqTree[] };
30+
export type PrereqTree =
31+
| string
32+
| { and: PrereqTree[] }
33+
| { or: PrereqTree[] }
34+
| { nOf: [number, PrereqTree[]] };
3135

3236
// Auxiliary data types
3337
export type Day =

website/src/utils/planner.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export const EXEMPTION_SEMESTER: Semester = -1;
1414
export const PLAN_TO_TAKE_YEAR = '3000';
1515
export const PLAN_TO_TAKE_SEMESTER = -2;
1616

17+
const GRADE_REQUIREMENT_SEPARATOR = ':';
18+
1719
// We assume iBLOCs takes place in special term 1
1820
export const IBLOCS_SEMESTER = 3;
1921

@@ -36,6 +38,10 @@ export function getSemesterName(semester: Semester) {
3638
export function checkPrerequisite(moduleSet: Set<ModuleCode>, tree: PrereqTree) {
3739
function walkTree(fragment: PrereqTree): PrereqTree[] | null {
3840
if (typeof fragment === 'string') {
41+
if (fragment.includes(GRADE_REQUIREMENT_SEPARATOR)) {
42+
const [module] = fragment.split(GRADE_REQUIREMENT_SEPARATOR);
43+
return moduleSet.has(module) ? null : [module];
44+
}
3945
return moduleSet.has(fragment) ? null : [fragment];
4046
}
4147

@@ -51,6 +57,12 @@ export function checkPrerequisite(moduleSet: Set<ModuleCode>, tree: PrereqTree)
5157
return notFulfilled.length === 0 ? null : flatten(notFulfilled);
5258
}
5359

60+
if ('nOf' in fragment) {
61+
const requiredCount = fragment.nOf[0];
62+
const fulfilled = fragment.nOf[1].map(walkTree).filter((x) => x === null);
63+
return fulfilled.length >= requiredCount ? null : [fragment];
64+
}
65+
5466
return assertNever(fragment);
5567
}
5668

@@ -71,6 +83,11 @@ export function conflictToText(conflict: PrereqTree): string {
7183
return conflict.and.map(conflictToText).join(' and ');
7284
}
7385

86+
if ('nOf' in conflict) {
87+
const [n, conflicts] = conflict.nOf;
88+
return `require ${n} of ${conflicts.map(conflictToText).join(', ')}`;
89+
}
90+
7491
return assertNever(conflict);
7592
}
7693

website/src/views/modules/ModulePageContent.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ import Title from 'views/components/Title';
2727
import { Archive, Check } from 'react-feather';
2828

2929
import useScrollToTop from 'views/hooks/useScrollToTop';
30+
import ErrorBoundary from 'views/errors/ErrorBoundary';
3031
import styles from './ModulePageContent.scss';
3132
import ReportError from './ReportError';
33+
import ModuleTree from './ModuleTree';
3234

3335
export type Props = {
3436
module: Module;
@@ -257,10 +259,7 @@ const ModulePageContent: React.FC<Props> = ({ module, archiveYear }) => {
257259
</section>
258260
</div>
259261

260-
{/* Disabled for now because a new parser needs to be written to
261-
process the new updated requisite string. */}
262-
263-
{/* <section className={styles.section} id={SIDE_MENU_ITEMS.prerequisites}>
262+
<section className={styles.section} id={SIDE_MENU_ITEMS.prerequisites}>
264263
<h2 className={styles.sectionHeading}>Prerequisite Tree</h2>
265264
<ErrorBoundary>
266265
<ModuleTree
@@ -269,14 +268,6 @@ const ModulePageContent: React.FC<Props> = ({ module, archiveYear }) => {
269268
fulfillRequirements={module.fulfillRequirements}
270269
/>
271270
</ErrorBoundary>
272-
</section> */}
273-
274-
<section className={styles.section} id={SIDE_MENU_ITEMS.prerequisites}>
275-
<h2 className={styles.sectionHeading}>Prerequisite Tree</h2>
276-
<p>
277-
The prerequisite tree is now being improved to support the new (and more accurate)
278-
prerequisite information provided by NUS. It will be back soon!
279-
</p>
280271
</section>
281272

282273
<section className={styles.section} id={SIDE_MENU_ITEMS.timetable}>

website/src/views/modules/ModuleTree.tsx

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,56 @@ interface TreeDisplay {
2020
isPrereq?: boolean;
2121
}
2222

23-
const formatConditional = (name: string) => (name === 'or' ? 'one of' : 'all of');
23+
const GRADE_REQUIREMENT_SEPARATOR = ':';
24+
const MODULE_NAME_WILDCARD = '%';
25+
const PASSING_GRADE = 'D';
26+
27+
const formatConditional = (node: PrereqTree) => {
28+
if (typeof node === 'string') return node;
29+
if ('nOf' in node) {
30+
const requiredNum = node.nOf[0];
31+
return `at least ${requiredNum} of`;
32+
}
33+
if ('or' in node) {
34+
return 'one of';
35+
}
36+
return 'all of';
37+
};
2438

25-
const nodeName = (node: PrereqTree) => (typeof node === 'string' ? node : Object.keys(node)[0]);
39+
const nodeName = (node: PrereqTree) => {
40+
if (typeof node !== 'string') {
41+
return Object.keys(node)[0];
42+
}
43+
let name = node;
44+
if (name.includes(GRADE_REQUIREMENT_SEPARATOR)) {
45+
const [moduleName, requiredGrade] = name.split(GRADE_REQUIREMENT_SEPARATOR);
46+
if (requiredGrade !== PASSING_GRADE) {
47+
name = `${moduleName} (grade of at least ${requiredGrade})`;
48+
} else {
49+
name = moduleName;
50+
}
51+
}
52+
if (name.includes(MODULE_NAME_WILDCARD)) {
53+
const [beforeWildcard, afterWildcard] = name.split(MODULE_NAME_WILDCARD);
54+
name = `course that starts with "${beforeWildcard}" ${afterWildcard}`;
55+
}
56+
return name.trim();
57+
};
2658

27-
const unwrapLayer = (node: PrereqTree) =>
28-
typeof node === 'string' ? [node] : flatten(values(node).filter(notNull));
59+
const unwrapLayer = (node: PrereqTree) => {
60+
if (typeof node === 'string') {
61+
return [node];
62+
}
63+
if ('nOf' in node) {
64+
return node.nOf[1];
65+
}
66+
return flatten(values(node).filter(notNull));
67+
};
2968

3069
const Branch: React.FC<{ nodes: PrereqTree[]; layer: number }> = (props) => (
3170
<ul className={styles.tree}>
32-
{props.nodes.map((child) => (
33-
<li className={styles.branch} key={nodeName(child)}>
71+
{props.nodes.map((child, idx) => (
72+
<li className={styles.branch} key={typeof child === 'string' ? nodeName(child) : idx}>
3473
<Tree node={child} layer={props.layer} />
3574
</li>
3675
))}
@@ -53,7 +92,7 @@ const Tree: React.FC<TreeDisplay> = (props) => {
5392
})}
5493
>
5594
{isConditional ? (
56-
formatConditional(name)
95+
formatConditional(node)
5796
) : (
5897
<LinkModuleCodes className={styles.link}>{name}</LinkModuleCodes>
5998
)}
@@ -96,9 +135,27 @@ const ModuleTree: React.FC<Props> = (props) => {
96135
</ul>
97136
</div>
98137

99-
<p className="alert alert-warning">
138+
{/* <p className="alert alert-warning">
100139
The prerequisite tree is displayed for visualization purposes and may not be accurate.
101140
Viewers are encouraged to double check details.
141+
</p> */}
142+
143+
<p className="alert alert-warning">
144+
This new version of the prerequisite tree is being tested and may not be accurate. Viewers
145+
are encouraged to double check details with the prerequisite text above. To report bugs with
146+
the new tree, please post a bug report on GitHub (preferred) at{' '}
147+
<a
148+
href="https://github.com/nusmodifications/nusmods/issues/new/choose"
149+
target="_blank"
150+
rel="noopener noreferrer nofollow"
151+
>
152+
our repository
153+
</a>{' '}
154+
or send an email to{' '}
155+
<a href="mailto:[email protected]" target="_blank" rel="noopener noreferrer nofollow">
156+
157+
</a>
158+
.
102159
</p>
103160
</>
104161
);

website/src/views/modules/__snapshots__/ModuleTree.test.tsx.snap

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,23 @@ Array [
351351
<p
352352
class="alert alert-warning"
353353
>
354-
The prerequisite tree is displayed for visualization purposes and may not be accurate. Viewers are encouraged to double check details.
354+
This new version of the prerequisite tree is being tested and may not be accurate. Viewers are encouraged to double check details with the prerequisite text above. To report bugs with the new tree, please post a bug report on GitHub (preferred) at
355+
<a
356+
href="https://github.com/nusmodifications/nusmods/issues/new/choose"
357+
rel="noopener noreferrer nofollow"
358+
target="_blank"
359+
>
360+
our repository
361+
</a>
362+
or send an email to
363+
<a
364+
href="mailto:[email protected]"
365+
rel="noopener noreferrer nofollow"
366+
target="_blank"
367+
>
368+
369+
</a>
370+
.
355371
</p>,
356372
]
357373
`;
@@ -573,7 +589,23 @@ Array [
573589
<p
574590
class="alert alert-warning"
575591
>
576-
The prerequisite tree is displayed for visualization purposes and may not be accurate. Viewers are encouraged to double check details.
592+
This new version of the prerequisite tree is being tested and may not be accurate. Viewers are encouraged to double check details with the prerequisite text above. To report bugs with the new tree, please post a bug report on GitHub (preferred) at
593+
<a
594+
href="https://github.com/nusmodifications/nusmods/issues/new/choose"
595+
rel="noopener noreferrer nofollow"
596+
target="_blank"
597+
>
598+
our repository
599+
</a>
600+
or send an email to
601+
<a
602+
href="mailto:[email protected]"
603+
rel="noopener noreferrer nofollow"
604+
target="_blank"
605+
>
606+
607+
</a>
608+
.
577609
</p>,
578610
]
579611
`;

0 commit comments

Comments
 (0)