• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Courseography / courseography / 13b93df2-a3c8-4626-b002-d74a41bf0fdb

29 Aug 2025 05:52PM UTC coverage: 54.631% (-2.4%) from 57.075%
13b93df2-a3c8-4626-b002-d74a41bf0fdb

push

circleci

web-flow
Added autocomplete course dropdown for Generate form (#1600)

485 of 952 branches covered (50.95%)

Branch coverage included in aggregate %.

18 of 18 new or added lines in 2 files covered. (100.0%)

84 existing lines in 1 file now uncovered.

2181 of 3928 relevant lines covered (55.52%)

160.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

31.17
/js/components/generate/GenerateForm.js
1
import React from "react"
2
import { ErrorMessage, Field, Form, Formik } from "formik"
3
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
4
import { faWandSparkles } from "@fortawesome/free-solid-svg-icons"
5
import { Tooltip } from "react-tooltip"
6
import { Graph, populateHybridRelatives } from "../graph/Graph"
7
import Disclaimer from "../common/Disclaimer"
8
import { NavBar } from "../common/NavBar.js.jsx"
9
import AutocompleteDropdown from "./AutocompleteDropdown.js"
10

11
export default class GenerateForm extends React.Component {
12
  constructor(props) {
13
    super(props)
18✔
14
    this.state = {
18✔
15
      fceCount: 0,
16
      selectedCourses: [],
17
    }
18

19
    this.graph = React.createRef()
18✔
20
  }
21

22
  setFCECount = credits => {
18✔
UNCOV
23
    this.setState({ fceCount: credits })
×
24
  }
25

26
  incrementFCECount = credits => {
18✔
27
    this.setState({ fceCount: this.state.fceCount + credits })
×
28
  }
29

30
  handleCoursesChange = newCourse => {
18✔
31
    this.setState({ selectedCourses: newCourse })
2✔
32
  }
33

34
  handleSubmit = (values, { setErrors }) => {
18✔
35
    const data = {}
3✔
36

37
    for (const key in values) {
3✔
38
      if (["courses", "programs", "taken", "departments"].includes(key)) {
27✔
39
        data[key] = values[key].split(",").map(s => s.trim())
13✔
40
      } else {
41
        data[key] = values[key]
15✔
42
      }
43
    }
44

45
    let submittedCourses = data["courses"]
3✔
46

47
    if (values.category === "programs") {
3✔
48
      data["courses"] = []
1✔
49
    } else {
50
      data["programs"] = []
2✔
51
    }
52

53
    const putData = {
3✔
54
      method: "PUT",
55
      headers: {
56
        "Content-Type": "application/json",
57
      },
58
      body: JSON.stringify(data), // We send data in JSON format
59
    }
60

61
    fetch("/graph-generate", putData)
3✔
UNCOV
62
      .then(res => res.json())
×
63
      .then(data => {
UNCOV
64
        const returnedTexts = data.texts?.map(t => t.text) ?? []
×
65

UNCOV
66
        const missingCourses = submittedCourses.filter(
×
UNCOV
67
          c => !returnedTexts.includes(c.toUpperCase())
×
68
        )
69

UNCOV
70
        if (missingCourses.length !== 0 && missingCourses[0] !== "") {
×
UNCOV
71
          setErrors({
×
72
            courses:
73
              missingCourses.length === 1
×
74
                ? `Invalid course code: ${missingCourses}`
75
                : `Invalid course codes: ${missingCourses.join(", ")}`,
76
          })
77
        }
78

UNCOV
79
        const missingPrograms = data.error?.invalidPrograms ?? []
×
80

UNCOV
81
        if (missingPrograms.length !== 0) {
×
82
          setErrors({
×
83
            programs:
84
              missingPrograms.length === 1
×
85
                ? `Invalid program code: ${missingPrograms}`
86
                : `Invalid program codes: ${missingPrograms.join(", ")}`,
87
          })
88
        }
89

UNCOV
90
        const labelsJSON = {}
×
UNCOV
91
        const regionsJSON = {}
×
UNCOV
92
        const nodesJSON = {}
×
UNCOV
93
        const hybridsJSON = {}
×
UNCOV
94
        const boolsJSON = {}
×
UNCOV
95
        const edgesJSON = {}
×
UNCOV
96
        const boolsStatus = {}
×
UNCOV
97
        const nodesStatus = {}
×
UNCOV
98
        const parentsObj = {}
×
UNCOV
99
        const inEdgesObj = {}
×
UNCOV
100
        const childrenObj = {}
×
UNCOV
101
        const outEdgesObj = {}
×
UNCOV
102
        const storedNodes = new Set()
×
103

UNCOV
104
        data.texts.forEach(entry => {
×
105
          // filter for mark percentages, allow preceding characters for potential geq
UNCOV
106
          if (entry.text.match(/.*[0-9]*%/g)) {
×
107
            labelsJSON[entry.rId] = entry
×
108
          }
109
        })
110

UNCOV
111
        data.shapes.forEach(function (entry) {
×
UNCOV
112
          if (entry.type_ === "Node") {
×
UNCOV
113
            nodesJSON[entry.id_] = entry
×
UNCOV
114
          } else if (entry.type_ === "Hybrid") {
×
115
            hybridsJSON[entry.id_] = entry
×
UNCOV
116
          } else if (entry.type_ === "BoolNode") {
×
UNCOV
117
            boolsStatus[entry.id_] = "inactive"
×
UNCOV
118
            boolsJSON[entry.id_] = entry
×
119
          }
120
        })
121

UNCOV
122
        data.paths.forEach(function (entry) {
×
UNCOV
123
          if (entry.isRegion) {
×
124
            regionsJSON[entry.id_] = entry
×
125
          } else {
UNCOV
126
            edgesJSON[entry.id_] = entry
×
127
          }
128
        })
129

UNCOV
130
        Object.values(nodesJSON).forEach(node => {
×
UNCOV
131
          parentsObj[node.id_] = []
×
UNCOV
132
          inEdgesObj[node.id_] = []
×
UNCOV
133
          childrenObj[node.id_] = []
×
UNCOV
134
          outEdgesObj[node.id_] = []
×
135
          // Quickly adding any active nodes from local storage into the selected nodes
UNCOV
136
          let state = localStorage.getItem(node.id_)
×
UNCOV
137
          if (state === null) {
×
UNCOV
138
            state = parentsObj[node.id_].length === 0 ? "takeable" : "inactive"
×
139
          } else if (state === "active") {
×
140
            storedNodes.add(node.text[node.text.length - 1].text)
×
141
          }
142

UNCOV
143
          nodesStatus[node.id_] = {
×
144
            status: state,
145
            selected: ["active", "overridden"].indexOf(state) >= 0,
146
          }
147
        })
148

UNCOV
149
        Object.values(hybridsJSON).forEach(hybrid => {
×
150
          childrenObj[hybrid.id_] = []
×
151
          outEdgesObj[hybrid.id_] = []
×
152
          const nodesList = Object.values(nodesJSON)
×
153
          populateHybridRelatives(hybrid, nodesList, parentsObj, childrenObj)
×
154
          let state = localStorage.getItem(hybrid.id_)
×
155
          if (state === null) {
×
156
            state = parentsObj[hybrid.id_].length === 0 ? "takeable" : "inactive"
×
157
          }
158
          nodesStatus[hybrid.id_] = {
×
159
            status: state,
160
            selected: ["active", "overridden"].indexOf(state) >= 0,
161
          }
162
        })
163

UNCOV
164
        Object.values(edgesJSON).forEach(edge => {
×
UNCOV
165
          if (edge.target in parentsObj) {
×
UNCOV
166
            parentsObj[edge.target].push(edge.source)
×
UNCOV
167
            inEdgesObj[edge.target].push(edge.id_)
×
168
          }
169

UNCOV
170
          if (edge.source in childrenObj) {
×
UNCOV
171
            childrenObj[edge.source].push(edge.target)
×
UNCOV
172
            outEdgesObj[edge.source].push(edge.id_)
×
173
          }
174
        })
175

UNCOV
176
        Object.keys(boolsJSON).forEach(boolId => {
×
UNCOV
177
          const parents = []
×
UNCOV
178
          const childs = []
×
UNCOV
179
          const outEdges = []
×
UNCOV
180
          const inEdges = []
×
UNCOV
181
          Object.values(edgesJSON).forEach(edge => {
×
UNCOV
182
            if (boolId === edge.target) {
×
UNCOV
183
              parents.push(edge.source)
×
UNCOV
184
              inEdges.push(edge.id_)
×
UNCOV
185
            } else if (boolId === edge.source) {
×
UNCOV
186
              childs.push(edge.target)
×
UNCOV
187
              outEdges.push(edge.id_)
×
188
            }
189
          })
UNCOV
190
          parentsObj[boolId] = parents
×
UNCOV
191
          childrenObj[boolId] = childs
×
UNCOV
192
          outEdgesObj[boolId] = outEdges
×
UNCOV
193
          inEdgesObj[boolId] = inEdges
×
194
        })
195

UNCOV
196
        const edgesStatus = Object.values(edgesJSON).reduce((acc, curr) => {
×
UNCOV
197
          const source = curr.source
×
UNCOV
198
          const target = curr.target
×
199
          let status
200
          const isSourceSelected =
UNCOV
201
            nodesStatus[source]?.selected || boolsStatus[source] === "active"
×
202

203
          const isTargetSelected =
UNCOV
204
            nodesStatus[target]?.selected || boolsStatus[target] === "active"
×
205

UNCOV
206
          const targetStatus = nodesStatus[target]?.status || boolsStatus[target]
×
207

UNCOV
208
          if (!isSourceSelected && targetStatus === "missing") {
×
209
            status = "missing"
×
UNCOV
210
          } else if (!isSourceSelected) {
×
UNCOV
211
            status = "inactive"
×
212
          } else if (!isTargetSelected) {
×
213
            status = "takeable"
×
214
          } else {
215
            status = "active"
×
216
          }
UNCOV
217
          acc[curr.id_] = status
×
UNCOV
218
          return acc
×
219
        }, {})
220

UNCOV
221
        this.graph.current.setState({
×
222
          labelsJSON: labelsJSON,
223
          regionsJSON: regionsJSON,
224
          nodesJSON: nodesJSON,
225
          hybridsJSON: hybridsJSON,
226
          boolsJSON: boolsJSON,
227
          edgesJSON: edgesJSON,
228
          nodesStatus: nodesStatus,
229
          edgesStatus: edgesStatus,
230
          boolsStatus: boolsStatus,
231
          width: data.width,
232
          height: data.height,
233
          zoomFactor: 1,
234
          horizontalPanFactor: 0,
235
          verticalPanFactor: 0,
236
          connections: {
237
            parents: parentsObj,
238
            inEdges: inEdgesObj,
239
            children: childrenObj,
240
            outEdges: outEdgesObj,
241
          },
242
          selectedNodes: storedNodes,
243
        })
244
      })
245
      .catch(err => {
246
        console.log("err :>> ", err)
×
247
      })
248
  }
249

250
  validateForm = values => {
18✔
251
    const errors = {}
23✔
252

253
    const coursePattern = /^[A-Za-z]{3}\d{3}[HYhy]\d$/
23✔
254
    const deptPattern = /^[A-Za-z]{3}$/
23✔
255
    const programPattern = /^[A-Za-z]{5}\d{4}[A-Za-z]?$/
23✔
256

257
    if (values.category === "courses") {
23✔
258
      if (!values.courses.trim().length) {
18✔
259
        errors.courses = "Cannot generate graph – no courses entered!"
16✔
260
      } else {
261
        const courses = values.courses.split(",").map(course => course.trim())
3✔
262
        const invalidCourses = courses.filter(course => !coursePattern.test(course))
3✔
263

264
        if (invalidCourses.length > 0) {
2!
UNCOV
265
          errors.courses =
×
266
            invalidCourses.length === 1
×
267
              ? `Invalid course code: ${invalidCourses}`
268
              : `Invalid course codes: ${invalidCourses.join(", ")}`
269
        }
270
      }
271
    }
272

273
    if (values.category === "programs") {
23✔
274
      if (!values.programs.trim().length) {
5✔
275
        errors.programs = "Cannot generate graph – no programs entered!"
2✔
276
      } else {
277
        const programs = values.programs.split(",").map(program => program.trim())
5✔
278
        const invalidPrograms = programs.filter(
3✔
279
          program => !programPattern.test(program)
5✔
280
        )
281

282
        if (invalidPrograms.length > 0) {
3✔
283
          errors.programs =
2✔
284
            invalidPrograms.length === 1
2!
285
              ? `Invalid program code: ${invalidPrograms}`
286
              : `Invalid program codes: ${invalidPrograms.join(", ")}`
287
        }
288
      }
289
    }
290

291
    if (values.departments && values.departments.trim()) {
23✔
292
      const departments = values.departments.split(",").map(dept => dept.trim())
28✔
293
      const invalidDepartments = departments.filter(dept => !deptPattern.test(dept))
28✔
294

295
      if (invalidDepartments.length > 0) {
8✔
296
        errors.departments =
6✔
297
          invalidDepartments.length === 1
6✔
298
            ? `Invalid department: ${invalidDepartments}`
299
            : `Invalid departments: ${invalidDepartments.join(", ")}`
300
      }
301
    }
302

303
    if (values.taken && values.taken.trim()) {
23!
UNCOV
304
      const takenCourses = values.taken.split(",").map(course => course.trim())
×
UNCOV
305
      const invalidTaken = takenCourses.filter(course => !coursePattern.test(course))
×
306

UNCOV
307
      if (invalidTaken.length > 0) {
×
UNCOV
308
        errors.taken =
×
309
          invalidTaken.length === 1
×
310
            ? `Invalid course code: ${invalidTaken}`
311
            : `Invalid course codes: ${invalidTaken.join(", ")}`
312
      }
313
    }
314

315
    return errors
23✔
316
  }
317

318
  render() {
319
    return (
20✔
320
      <>
321
        <NavBar selected_page="generate" open_modal={undefined}></NavBar>
322
        <div style={{ display: "flex", flexDirection: "row", height: "100%" }}>
323
          <Disclaimer />
324
          <div
325
            id="generateDiv"
326
            style={{
327
              position: "initial",
328
              padding: "0 0.5em",
329
              height: "100%",
330
              fontSize: "12pt",
331
            }}
332
          >
333
            <Formik
334
              initialValues={{
335
                category: "courses",
336
                courses: "",
337
                programs: "",
338
                taken: "",
339
                departments: "",
340
                maxDepth: 0,
341
                location: ["utsg"],
342
                includeRaws: false,
343
                includeGrades: false,
344
              }}
345
              validate={this.validateForm}
346
              validateOnChange={false}
347
              validateOnBlur={false}
348
              onSubmit={this.handleSubmit}
349
            >
350
              {({ values }) => (
351
                <Form id="generateForm">
218✔
352
                  <div className="form-section">
353
                    <div className="title-container">
354
                      <h1 id="header-title" className="section-title">
355
                        Select Search Input
356
                      </h1>
357
                      <a
358
                        data-tooltip-id="category-tooltip"
359
                        data-tooltip-html="Select between courses and programs to search for"
360
                        className="tooltip-icon"
361
                        style={{ marginTop: "-0.3rem" }}
362
                      ></a>
363
                      <Tooltip id="category-tooltip" place="right" />
364
                    </div>
365
                    <Field as="select" id="category" name="category">
366
                      <option value="courses">Courses</option>
367
                      <option value="programs">Programs</option>
368
                    </Field>
369

370
                    {values.category === "courses" && (
347✔
371
                      <>
372
                        <div className="title-container">
373
                          <h1 id="header-title" className="section-title">
374
                            Search Courses
375
                          </h1>
376
                          <a
377
                            data-tooltip-id="courses-tooltip"
378
                            data-tooltip-html="Generate the prerequisites for the given course(s).<br />
379
                        Each course code must follow the format CSC108H1<br />
380
                        (i.e. department + code + session)"
381
                            className="tooltip-icon"
382
                            style={{ marginTop: "-0.3rem" }}
383
                          ></a>
384
                          <Tooltip id="courses-tooltip" place="right" />
385
                        </div>
386
                        <AutocompleteDropdown
387
                          id="courses"
388
                          aria-label="courses"
389
                          name="courses"
390
                          placeholder="e.g., CSC207H1, CSC324H1"
391
                          onSelectedChange={this.handleCoursesChange}
392
                          className="autocomplete"
393
                        />
394
                        <div className="error-container">
395
                          <ErrorMessage
396
                            className="error-message"
397
                            name="courses"
398
                            type="text"
399
                            component="div"
400
                          />
401
                        </div>
402
                        <h1 className="chosen-courses">
403
                          Selected Courses: {this.state.selectedCourses.join(", ")}
404
                        </h1>
405
                      </>
406
                    )}
407

408
                    {values.category === "programs" && (
307✔
409
                      <>
410
                        <div className="title-container">
411
                          <h1 id="header-title" className="section-title">
412
                            Search Programs
413
                          </h1>
414
                          <a
415
                            data-tooltip-id="programs-tooltip"
416
                            data-tooltip-html="Generate the requirements for the given program(s).<br />
417
                        Each program code must follow the format ASMAJ1689"
418
                            className="tooltip-icon"
419
                            style={{ marginTop: "-0.3rem" }}
420
                          ></a>
421
                          <Tooltip id="programs-tooltip" place="right" />
422
                        </div>
423
                        <Field
424
                          id="programs"
425
                          name="programs"
426
                          type="text"
427
                          placeholder="e.g., ASMAJ1689, ASFOC1689B"
428
                        />
429
                        <div className="error-container">
430
                          <ErrorMessage
431
                            className="error-message"
432
                            name="programs"
433
                            component="div"
434
                          />
435
                        </div>
436
                      </>
437
                    )}
438
                  </div>
439

440
                  <div className="form-section">
441
                    <h2 id="filter-title" className="section-title">
442
                      Filters
443
                    </h2>
444

445
                    <div className="title-container">
446
                      <label htmlFor="departments">Departments</label>
447
                      <a
448
                        data-tooltip-id="departments-tooltip"
449
                        data-tooltip-html="Only include courses from these departments.<br />
450
                        Department codes must be 3 letters, seperated by commas."
451
                        className="tooltip-icon"
452
                      ></a>
453
                      <Tooltip id="departments-tooltip" place="right" />
454
                    </div>
455
                    <Field
456
                      id="departments"
457
                      name="departments"
458
                      type="text"
459
                      placeholder="e.g., CSC, MAT, STA"
460
                    />
461
                    <div className="error-container">
462
                      <ErrorMessage
463
                        className="error-message"
464
                        name="departments"
465
                        component="div"
466
                      />
467
                    </div>
468

469
                    <div className="title-container">
470
                      <label htmlFor="taken">Hide courses</label>
471
                      <a
472
                        data-tooltip-id="taken-tooltip"
473
                        data-tooltip-html="Do not show these courses or their prerequisites.<br />
474
                        Each course code must follow the format CSC108H1<br />
475
                        (i.e. department + code + session)"
476
                        className="tooltip-icon"
477
                      ></a>
478
                      <Tooltip
479
                        id="taken-tooltip"
480
                        className="tooltip-box"
481
                        place="right"
482
                      />
483
                    </div>
484
                    <Field
485
                      id="taken"
486
                      name="taken"
487
                      type="text"
488
                      placeholder="e.g., CSC207H1, CSC236H1"
489
                    />
490
                    <div className="error-container">
491
                      <ErrorMessage
492
                        className="error-message"
493
                        name="taken"
494
                        component="div"
495
                      />
496
                    </div>
497

498
                    <div className="title-container">
499
                      <label htmlFor="maxDepth">Prerequisite depth</label>
500
                      <a
501
                        data-tooltip-id="maxDepth-tooltip"
502
                        data-tooltip-content="Depth of prerequisite chain (0 shows all prerequisites)"
503
                        className="tooltip-icon"
504
                      ></a>
505
                      <Tooltip id="maxDepth-tooltip" place="right" />
506
                    </div>
507
                    <Field
508
                      id="maxDepth"
509
                      name="maxDepth"
510
                      type="number"
511
                      min="0"
512
                      step="1"
513
                    />
514

515
                    {/* <label htmlFor="location">Campus</label>
516
                  <Field id="location" name="location" as="select" multiple
517
                    style={{ verticalAlign: 'text-top', marginLeft: '1em', marginBottom: '1em', color: 'black' }}>
518
                    <option value="utsg">St. George</option>
519
                    <option value="utm">Mississauga</option>
520
                    <option value="utsc">Scarborough</option>
521
                  </Field>
522

523
                  <p>
524
                    <label htmlFor="includeRaws">Include non-course prerequisites</label>
525
                    <Field id="includeRaws" name="includeRaws" type="checkbox"
526
                      style={{ marginLeft: '1em', verticalAlign: 'middle' }}
527
                    />
528
                  </p>
529

530
                  <label htmlFor="includeGrades">Include grade-based prerequisites</label>
531
                  <Field id="includeGrades" name="includeGrades" type="checkbox"
532
                    style={{ 'margin-left': '1em', 'vertical-align': 'middle' }} /> */}
533
                  </div>
534

535
                  <div
536
                    style={{
537
                      width: "100%",
538
                      display: "flex",
539
                      justifyContent: "center",
540
                    }}
541
                  >
542
                    <button id="submit" type="submit">
543
                      <FontAwesomeIcon icon={faWandSparkles} id="generate-icon" />
544
                      Generate
545
                    </button>
546
                  </div>
547
                </Form>
548
              )}
549
            </Formik>
550
          </div>
551

552
          <Graph
553
            ref={this.graph}
554
            start_blank={true}
555
            fceCount={this.state.fceCount}
556
            incrementFCECount={this.incrementFCECount}
557
            setFCECount={this.setFCECount}
558
          />
559
        </div>
560
      </>
561
    )
562
  }
563
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc