├── .circleci
└── config.yml
├── .git-blame-ignore-revs
├── .github
├── PULL_REQUEST_TEMPLATE.md
└── dependabot.yml
├── .gitignore
├── .hlint.yaml
├── .husky
├── .gitignore
└── pre-commit
├── .prettierignore
├── .prettierrc
├── .stylelintrc.json
├── .stylish-haskell.yaml
├── .yarn
└── releases
│ └── yarn-3.3.1.cjs
├── .yarnrc.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── PRIVACY.md
├── README.md
├── Setup.hs
├── app
├── Config.hs
├── Controllers
│ ├── Course.hs
│ ├── Generate.hs
│ ├── Graph.hs
│ └── Timetable.hs
├── Css
│ └── Constants.hs
├── Database
│ ├── CourseInsertion.hs
│ ├── CourseQueries.hs
│ ├── CourseVideoSeed.hs
│ ├── DataType.hs
│ ├── Database.hs
│ ├── README.md
│ ├── Requirement.hs
│ └── Tables.hs
├── DynamicGraphs
│ ├── CourseFinder.hs
│ ├── GraphGenerator.hs
│ ├── GraphNodeUtils.hs
│ ├── GraphOptions.hs
│ └── WriteRunDot.hs
├── Export
│ ├── GetImages.hs
│ ├── ImageConversion.hs
│ ├── LatexGenerator.hs
│ ├── PdfGenerator.hs
│ ├── README.md
│ └── TimetableImageCreator.hs
├── Main.hs
├── MasterTemplate.hs
├── Models
│ └── Course.hs
├── Response.hs
├── Response
│ ├── About.hs
│ ├── Draw.hs
│ ├── Image.hs
│ ├── Loading.hs
│ ├── NotFound.hs
│ └── README.md
├── Routes.hs
├── Scripts.hs
├── Server.hs
├── Svg
│ ├── Builder.hs
│ ├── Database.hs
│ ├── Generator.hs
│ ├── Parser.hs
│ └── README.md
├── Util
│ ├── Blaze.hs
│ ├── Documentation.hs
│ ├── Happstack.hs
│ └── README.md
└── WebParsing
│ ├── ArtSciParser.hs
│ ├── Ligature.hs
│ ├── ParsecCombinators.hs
│ ├── PostParser.hs
│ ├── ReqParser.hs
│ └── UtsgJsonParser.hs
├── babel.config.json
├── backend-test
├── Controllers
│ ├── ControllerTests.hs
│ ├── CourseControllerTests.hs
│ └── GraphControllerTests.hs
├── Database
│ ├── CourseQueriesTests.hs
│ └── DatabaseTests.hs
├── Main.hs
├── RequirementTests
│ ├── ModifierTests.hs
│ ├── PostParserTests.hs
│ ├── PreProcessingTests.hs
│ ├── ReqParserTests.hs
│ └── RequirementTests.hs
├── SvgTests
│ ├── IntersectionTests.hs
│ └── SvgTests.hs
└── TestHelpers.hs
├── config.yaml
├── courseography.cabal
├── cypress.json
├── cypress
└── integration
│ └── graph_spec
│ ├── bool_spec.js
│ ├── edge_spec.js
│ ├── graph_spec.js
│ └── node_spec.js
├── db
└── building.csv
├── eslint.config.cjs
├── graphs
├── Estonian.graphml
├── Finnish.graphml
├── German.graphml
├── Italian.graphml
├── Linguistics.graphml
├── Rotman.graphml
├── abs2015.graphml
├── abs2015.svg
├── bch2015.graphml
├── bch2015.svg
├── csb2015.graphml
├── csb2015.svg
├── csc2014.graphml
├── csc2014.svg
├── csc2015.graphml
├── csc2015.svg
├── csc2016.graphml
├── csc2016.svg
├── csc2017.graphml
├── csc2017.svg
├── csc2019.graphml
├── csc2019.svg
├── csc2020.graphml
├── csc2020.svg
├── csc2021.svg
├── csc2023.graphml
├── csc2023.svg
├── csc2024.graphml
├── csc2024.svg
├── eas2015.graphml
├── eas2015.svg
├── eco2015.graphml
├── eco2015.svg
├── eng2015.graphml
├── eng2015.svg
├── est2015.svg
├── fin2015.svg
├── fre2015.graphml
├── fre2015.svg
├── ger2015.svg
├── ggr2015.graphml
├── ggr2015.svg
├── his2015.graphml
├── his2015.svg
├── hps2015.graphml
├── hps2015.svg
├── ita2015.svg
├── lin2015.graphml
├── lin2015.svg
├── math_specialist2022.svg
├── matt_specialist2022.graphml
├── prt2015.graphml
├── prt2015.svg
├── rotman2015.svg
├── sla2015.graphml
├── sla2015.svg
├── spa2015.graphml
├── spa2015.svg
├── sta2015.graphml
├── sta2015.svg
├── sta2017.graphml
├── sta2017.svg
├── sta2022.graphml
└── sta2022.svg
├── jest.config.js
├── js
├── components
│ ├── about
│ │ └── about.js
│ ├── common
│ │ ├── Disclaimer.js
│ │ ├── __tests__
│ │ │ ├── ConvertToLink.test.js
│ │ │ ├── CourseModalButtons.test.js
│ │ │ ├── GetTable.test.js
│ │ │ └── TimetableLoading.test.js
│ │ ├── export.js.jsx
│ │ ├── react_modal.js.jsx
│ │ └── utils.js
│ ├── draw
│ │ ├── draw.js
│ │ └── main.js
│ ├── generate
│ │ ├── GenerateForm.js
│ │ ├── __mocks__
│ │ │ └── sample_responses.json
│ │ ├── __tests__
│ │ │ └── generate.test.js
│ │ └── generate.jsx
│ ├── graph
│ │ ├── Bool.js
│ │ ├── Button.js
│ │ ├── Container.js
│ │ ├── Edge.js
│ │ ├── FocusBar.js
│ │ ├── FocusTab.js
│ │ ├── Graph.js
│ │ ├── GraphDropdown.js
│ │ ├── InfoBox.js
│ │ ├── Node.js
│ │ ├── Sidebar.js
│ │ ├── __mocks__
│ │ │ ├── aaa100-course-info.js
│ │ │ ├── bbb100-course-info.js
│ │ │ ├── ccc100-course-info.js
│ │ │ ├── defaultTestData.js
│ │ │ ├── focusData.js
│ │ │ ├── statisticsTestData.js
│ │ │ ├── styleMock.js
│ │ │ └── testContainerData.js
│ │ ├── __tests__
│ │ │ ├── Bool.test.js
│ │ │ ├── Button.test.js
│ │ │ ├── Edge.test.js
│ │ │ ├── FocusBar.test.js
│ │ │ ├── FocusTab.test.js
│ │ │ ├── Graph.test.js
│ │ │ ├── GraphDropdown.test.js
│ │ │ ├── InfoBox.test.js
│ │ │ ├── Node.test.js
│ │ │ ├── Sidebar.test.js
│ │ │ ├── TestContainer.js
│ │ │ ├── TestFocusBar.js
│ │ │ ├── TestGraph.js
│ │ │ ├── TestSidebar.js
│ │ │ ├── __snapshots__
│ │ │ │ ├── Bool.test.js.snap
│ │ │ │ ├── Button.test.js.snap
│ │ │ │ ├── Edge.test.js.snap
│ │ │ │ ├── FocusBar.test.js.snap
│ │ │ │ ├── Graph.test.js.snap
│ │ │ │ ├── GraphDropdown.test.js.snap
│ │ │ │ ├── InfoBox.test.js.snap
│ │ │ │ ├── Node.test.js.snap
│ │ │ │ └── Sidebar.test.js.snap
│ │ │ ├── cleanup-after-each.js
│ │ │ ├── findRelationship.test.js
│ │ │ └── populateHybridRelatives.test.js
│ │ └── main.js
│ └── grid
│ │ ├── calendar.js.jsx
│ │ ├── course_panel.js.jsx
│ │ └── grid.js.jsx
├── setupTests.js
├── setupTestsAfterEnv.js
└── util
│ ├── parse.test.js
│ └── util.js
├── package.json
├── public
├── js
│ ├── draw
│ │ ├── draw.js
│ │ ├── path.js
│ │ ├── setup.js
│ │ └── variables.js
│ └── vendor
│ │ └── jscolor.min.js
├── res
│ ├── backgrounds
│ │ └── draw-background.png
│ ├── graphs
│ │ └── gen
│ │ │ ├── 1.svg
│ │ │ ├── 10.svg
│ │ │ ├── 11.svg
│ │ │ ├── 12.svg
│ │ │ ├── 13.svg
│ │ │ ├── 14.svg
│ │ │ ├── 15.svg
│ │ │ ├── 16.svg
│ │ │ ├── 17.svg
│ │ │ ├── 18.svg
│ │ │ ├── 19.svg
│ │ │ ├── 2.svg
│ │ │ ├── 20.svg
│ │ │ ├── 21.svg
│ │ │ ├── 22.svg
│ │ │ ├── 23.svg
│ │ │ ├── 24.svg
│ │ │ ├── 25.svg
│ │ │ ├── 26.svg
│ │ │ ├── 3.svg
│ │ │ ├── 4.svg
│ │ │ ├── 5.svg
│ │ │ ├── 6.svg
│ │ │ ├── 7.svg
│ │ │ ├── 8.svg
│ │ │ └── 9.svg
│ ├── ico
│ │ ├── about.png
│ │ ├── blue-marker.png
│ │ ├── check.png
│ │ ├── delete.png
│ │ ├── doubledown-darkpurple.png
│ │ ├── doubledown-lightpurple.png
│ │ ├── doubleup-darkpurple.png
│ │ ├── doubleup-lightpurple.png
│ │ ├── export.png
│ │ ├── favicon.png
│ │ ├── red-marker.png
│ │ ├── reset-view.png
│ │ ├── search.png
│ │ ├── shadow.png
│ │ ├── sidebar.png
│ │ └── tempSidebar.png
│ └── img
│ │ ├── C-logo-small.png
│ │ ├── C-logo.png
│ │ ├── compass-small.png
│ │ ├── compass.png
│ │ ├── dcs.png
│ │ ├── dcs.svg
│ │ └── logo.png
└── style
│ └── bootstrap.min.3.1.1.css
├── scripts
└── BuildingGeocoder.py
├── stack.yaml
├── stack.yaml.lock
├── style
├── app.js
└── app.scss
├── test.config.yaml
├── tests
├── common_tests.js
└── tests.html
├── vendor
└── sendfile-0.7.9
│ ├── LICENSE
│ ├── Setup.hs
│ ├── sendfile.cabal
│ └── src
│ └── Network
│ └── Socket
│ ├── SendFile.hs
│ └── SendFile
│ ├── Darwin.hsc
│ ├── FreeBSD.hs
│ ├── Handle.hs
│ ├── Internal.hs
│ ├── Iter.hs
│ ├── Linux.hsc
│ ├── Portable.hs
│ ├── Util.hs
│ └── Win32.hsc
├── webpack.common.js
├── webpack.config.js
├── webpack.dev.js
├── webpack.prod.js
└── yarn.lock
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | orbs:
4 | coveralls: coveralls/coveralls@2.2.5
5 |
6 | workflows:
7 | tests:
8 | jobs:
9 | - build:
10 | context: coveralls-context
11 |
12 | jobs:
13 | build:
14 | docker:
15 | - image: fpco/stack-build:lts-23
16 | steps:
17 | - checkout
18 | - run: git remote add upstream https://github.com/Courseography/courseography.git
19 | - run: git fetch upstream
20 |
21 | - restore_cache:
22 | name: Restore Cached Dependencies (Haskell)
23 | keys:
24 | - v1.4-courseography-haskell-{{ checksum "courseography.cabal" }}-{{ checksum "stack.yaml.lock" }}
25 |
26 | - run:
27 | name: Install Package Dependencies
28 | command: |
29 | sudo apt-get update
30 | sudo apt-get install -y texlive-latex-base
31 | sudo wget https://imagemagick.org/archive/binaries/magick
32 | sudo cp magick /usr/local/bin
33 | sudo chmod 777 /usr/local/bin/magick
34 | sudo dpkg --remove --force-remove-reinstreq nodejs
35 | stack update
36 |
37 | curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - &&\
38 | sudo dpkg --remove --force-remove-reinstreq libnode72:amd64
39 | sudo apt-get install -y nodejs
40 |
41 | corepack enable
42 | corepack prepare yarn@stable --activate
43 |
44 | - run:
45 | name: Resolve/Update Dependencies
46 | command: |
47 | stack --no-terminal setup
48 | rm -fr $(stack path --dist-dir) $(stack path --local-install-root)
49 | stack --no-terminal build --fast -j1 --ghc-options -Werror --ghc-options -Wwarn=x-partial
50 | stack build hlint
51 | stack build hpc-lcov
52 | yarn install
53 |
54 | - run:
55 | name: Run Tests and Generate Coverage
56 | command: |
57 | yarn test --maxWorkers=4
58 | stack --no-terminal test --coverage
59 | $(stack exec which hpc-lcov)
60 |
61 | - coveralls/upload
62 |
63 | - save_cache:
64 | name: Cache Dependencies (Haskell)
65 | key: v1.4-courseography-haskell-{{ checksum "courseography.cabal" }}-{{ checksum "stack.yaml.lock" }}
66 | paths:
67 | - "/root/.stack"
68 | - ".stack-work"
69 |
70 | - run:
71 | name: Run lint-staged checks
72 | command: |
73 | npx lint-staged --diff="upstream/master...$(git branch --show-current)"
74 |
75 | - run:
76 | name: Generate documentation
77 | command: |
78 | stack exec haddock -- -o doc -h --optghc=-iapp --optghc=-XOverloadedStrings --optghc=-XPartialTypeSignatures --optghc=-XScopedTypeVariables --ignore-all-exports app/Main.hs
79 |
80 | - store_artifacts:
81 | path: doc
82 | destination: docs
83 | # The resource_class feature allows configuring CPU and RAM resources for each job. Different resource classes are available for different executors. https://circleci.com/docs/2.0/configuration-reference/#resourceclass
84 | resource_class: large
85 |
--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # Initial pre-commit and style checks
2 | 363a10775a2abd87b5db9a764e29f50cc602fd3a
3 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Proposed Changes
2 |
3 | _(Describe your changes here. Also describe the motivation for your changes: what problem do they solve, or how do they improve the application or codebase? If this pull request fixes an open issue, [use a keyword to link this pull request to the issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword).)_
4 |
5 | ...
6 |
7 |
8 | Screenshots of your changes (if applicable)
9 |
10 |
11 |
12 | ## Type of Change
13 |
14 | _(Write an `X` or a brief description next to the type or types that best describe your changes.)_
15 |
16 | | Type | Applies? |
17 | | --------------------------------------------------------------------------------------- | -------- |
18 | | 🚨 _Breaking change_ (fix or feature that would cause existing functionality to change) | |
19 | | ✨ _New feature_ (non-breaking change that adds functionality) | |
20 | | 🐛 _Bug fix_ (non-breaking change that fixes an issue) | |
21 | | 🎨 _User interface change_ (change to user interface; provide screenshots) | |
22 | | ♻️ _Refactoring_ (internal change to codebase, without changing functionality) | |
23 | | 🚦 _Test update_ (change that _only_ adds or modifies tests) | |
24 | | 📦 _Dependency update_ (change that updates a dependency) | |
25 | | 🔧 _Internal_ (change that _only_ affects developers or continuous integration) | |
26 |
27 | ## Checklist
28 |
29 | _(Complete each of the following items for your pull request. Indicate that you have completed an item by changing the `[ ]` into a `[x]` in the raw text, or by clicking on the checkbox in the rendered description on GitHub.)_
30 |
31 | Before opening your pull request:
32 |
33 | - [ ] I have performed a self-review of my changes.
34 | - Check that all changed files included in this pull request are intentional changes.
35 | - Check that all changes are relevant to the purpose of this pull request, as described above.
36 | - [ ] I have added tests for my changes, if applicable.
37 | - This is **required** for all bug fixes and new features.
38 | - [ ] I have updated the project documentation, if applicable.
39 | - This is **required** for new features.
40 | - [ ] If this is my first contribution, I have added myself to the list of contributors.
41 | - [ ] I have updated the project Changelog (this is required for all changes).
42 |
43 | After opening your pull request:
44 |
45 | - [ ] I have verified that the CircleCI checks have passed.
46 | - [ ] I have [requested a review](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) from a project maintainer.
47 |
48 | ## Questions and Comments
49 |
50 | _(Include any questions or comments you have regarding your changes.)_
51 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "monthly"
12 | groups:
13 | ag-grid:
14 | patterns:
15 | - "ag-grid-*"
16 | babel:
17 | patterns:
18 | - "@babel/*"
19 | eslint:
20 | patterns:
21 | - "eslint"
22 | - "eslint-*"
23 | fort-awesome:
24 | patterns:
25 | - "@fortawesome/*"
26 | jest:
27 | patterns:
28 | - "jest"
29 | - "jest-*"
30 | react:
31 | patterns:
32 | - "react"
33 | - "react-dom"
34 | stylelint:
35 | patterns:
36 | - "stylelint*"
37 | webpack:
38 | patterns:
39 | - "webpack"
40 | - "webpack-*"
41 | - "*-loader"
42 |
43 | - package-ecosystem: "github-actions"
44 | directory: "/"
45 | schedule:
46 | interval: "monthly"
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # cabal sandbox
2 | .cabal-sandbox/
3 | cabal.sandbox.config
4 |
5 | # local database
6 | *.sqlite3
7 | *.sqlite3-shm
8 | *.sqlite3-wal
9 |
10 | # generated build files
11 | /app/dist/
12 |
13 | # generated stack files
14 | .stack-work/
15 |
16 | # generated CSS
17 | /public/style/
18 |
19 | # generated resources
20 | /graphs/gen/*.svg
21 | /graphs/gen/*.png
22 |
23 | # course videos
24 | /public/videos/
25 |
26 | # generated documentation
27 | /doc
28 |
29 | # IDE project files
30 | .idea
31 | .vscode
32 |
33 | # Node (Javascript) modules
34 | /node_modules
35 |
36 | # Yarn files
37 | .yarn/*
38 | .yarn/cache
39 | !.yarn/patches
40 | !.yarn/plugins
41 | !.yarn/releases
42 | !.yarn/sdks
43 | !.yarn/versions
44 |
45 | # TODO: eventually remove all public/js files from tracking
46 | /public/js/search
47 | /public/js/grid/app.js
48 | /public/js/post/app.js
49 | /public/js/generate/app.js
50 | /public/js/graph/app.js
51 | /public/js/draw/app.js
52 | /public/js/about/app.js
53 | /public/*.svg
54 | /public/*.png
55 | /public/*.ttf
56 | /public/*.eot
57 | /public/*.woff
58 | /public/*.png
59 | /public/*.woff2
60 |
61 | # coverage directory used by tools like istanbul and jest --coverage
62 | coverage
63 |
64 | # cypress suggested folder structure scaffolding
65 | /cypress/fixtures
66 | /cypress/integration/examples
67 | /cypress/plugins
68 | /cypress/support
69 |
70 | # Python virtual environment
71 | venv
72 |
73 | # Other generated files
74 | .eslintcache
75 |
--------------------------------------------------------------------------------
/.hlint.yaml:
--------------------------------------------------------------------------------
1 | # HLint configuration file
2 | # https://github.com/ndmitchell/hlint
3 | ##########################
4 |
5 | # This file contains a template configuration file, which is typically
6 | # placed as .hlint.yaml in the root of your project
7 |
8 | # Specify additional command line arguments
9 | #
10 | # - arguments: [--color, --cpp-simple, -XQuasiQuotes]
11 |
12 | # Control which extensions/flags/modules/functions can be used
13 | #
14 | # - extensions:
15 | # - default: false # all extension are banned by default
16 | # - name: [PatternGuards, ViewPatterns] # only these listed extensions can be used
17 | # - {name: CPP, within: CrossPlatform} # CPP can only be used in a given module
18 | #
19 | # - flags:
20 | # - {name: -w, within: []} # -w is allowed nowhere
21 | #
22 | # - modules:
23 | # - {name: [Data.Set, Data.HashSet], as: Set} # if you import Data.Set qualified, it must be as 'Set'
24 | # - {name: Control.Arrow, within: []} # Certain modules are banned entirely
25 | #
26 | # - functions:
27 | # - {name: unsafePerformIO, within: []} # unsafePerformIO can only appear in no modules
28 |
29 | # Add custom hints for this project
30 | #
31 | # Will suggest replacing "wibbleMany [myvar]" with "wibbleOne myvar"
32 | # - error: {lhs: "wibbleMany [x]", rhs: wibbleOne x}
33 |
34 | # The hints are named by the string they display in warning messages.
35 | # For example, if you see a warning starting like
36 | #
37 | # Main.hs:116:51: Warning: Redundant ==
38 | #
39 | # You can refer to that hint with `{name: Redundant ==}` (see below).
40 |
41 | # Turn on hints that are off by default
42 | #
43 | # Ban "module X(module X) where", to require a real export list
44 | # - warn: {name: Use explicit module export list}
45 | #
46 | # Replace a $ b $ c with a . b $ c
47 | # - group: {name: dollar, enabled: true}
48 | #
49 | # Generalise map to fmap, ++ to <>
50 | # - group: {name: generalise, enabled: true}
51 |
52 | # Ignore some builtin hints
53 | # - ignore: {name: Use let}
54 | # - ignore: {name: Use const, within: SpecialModule} # Only within certain modules
55 | - ignore: { name: "Use section" }
56 | - ignore: { name: "Use bimap" }
57 | # Define some custom infix operators
58 | # - fixity: infixr 3 ~^#^~
59 |
60 | # To generate a suitable file for HLint do:
61 | # $ hlint --default > .hlint.yaml
62 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npx lint-staged
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | app/
2 | db/
3 | doc/
4 | graphs/
5 |
6 | # TODO: review this file
7 | js/components/draw/
8 |
9 | public/
10 |
11 | vendor/
12 | *.min.*
13 |
14 | .stack-work/
15 | .yarn/
16 | .pnp.cjs
17 | .pnp.loader.mjs
18 |
19 | venv/
20 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 88,
3 | "semi": false,
4 | "arrowParens": "avoid",
5 | "trailingComma": "es5"
6 | }
7 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["stylelint-config-standard-scss"],
3 | "rules": {
4 | "no-descending-specificity": null,
5 | "selector-class-pattern": null,
6 | "selector-id-pattern": null,
7 | "selector-max-compound-selectors": 3,
8 | "selector-max-id": 2,
9 | "selector-no-qualifying-type": null
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
3 | yarnPath: .yarn/releases/yarn-3.3.1.cjs
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | ## Getting Involved
4 |
5 | It's recommended that you get involved if you aren't already. Click [here](https://github.com/Courseography/courseography/wiki/Getting-Involved) to learn more.
6 |
7 | ## Style
8 |
9 | Please ensure that your code follows the same style conventions as outlined [here](https://github.com/Courseography/courseography/wiki#style-guides).
10 |
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
1 | #Privacy Policy
2 |
3 | Courseography does not store any information about you: none of your actions in the various components of this website are recorded, nor is any of your information accessed through Facebook.
4 |
5 | There is an option for you to post schedules and graphs to your Facebook account. At no point will Courseography post anything to Facebook without your instruction to do so. Courseography does not interact with your Facebook account in any way other than these posts, and logging in and out. Logging into your Facebook account is not required to use any other feature of this website.
6 |
7 | ##Changes to this privacy statement
8 |
9 | We may amend this Privacy Statement from time to time. If we make changes in the way we use personal information, we will notify you by posting an announcement on our website. Users are bound by any changes to the Privacy Statement when he or she uses or otherwise accesses Courseography after such changes have been first posted.
10 |
11 | ##Questions or concerns
12 |
13 | If you have any questions or concerns regarding privacy on our website, please send us a detailed message at courseography@cs.toronto.edu. We will make every effort to resolve your concerns.
14 |
15 | Effective Date: April 15, 2015
16 |
--------------------------------------------------------------------------------
/Setup.hs:
--------------------------------------------------------------------------------
1 | import Distribution.Simple
2 | import System.Exit
3 | import System.Process (readProcessWithExitCode)
4 | import Distribution.PackageDescription (emptyHookedBuildInfo)
5 | import System.Directory (doesFileExist, copyFile)
6 |
7 | main = defaultMainWithHooks
8 | simpleUserHooks { preBuild = preBuildChecks }
9 | where
10 | -- | Checks that Imagemagick and LaTeX are available
11 | preBuildChecks _ _ = do
12 | mapM_ checkDependency ["magick", "pdflatex"]
13 | return emptyHookedBuildInfo
14 |
15 | checkDependency :: String -> IO ()
16 | checkDependency dependency = do
17 | (result, _, _) <- readProcessWithExitCode dependency ["-version"] ""
18 | case result of
19 | ExitFailure 127 -> putStrLn ("Error Message: " ++ dependency ++ " is NOT available. Please ensure it is on your path.") >> exitFailure
20 | _ -> putStrLn (dependency ++ " has been installed.")
21 |
--------------------------------------------------------------------------------
/app/Controllers/Course.hs:
--------------------------------------------------------------------------------
1 | module Controllers.Course
2 | (retrieveCourse, index, courseInfo) where
3 |
4 | import Config (runDb)
5 | import Control.Monad.IO.Class (liftIO)
6 | import qualified Data.Text as T (Text, unlines)
7 | import qualified Database.CourseQueries as CourseHelpers (getDeptCourses)
8 | import Database.Persist (Entity)
9 | import Database.Persist.Sqlite (SqlPersistM, entityVal, selectList)
10 | import Database.Tables as Tables (Courses, coursesCode)
11 | import Happstack.Server (Response, ServerPart, lookText', toResponse)
12 | import Models.Course (returnCourse)
13 | import Util.Happstack (createJSONResponse)
14 |
15 | -- | Takes a course code (e.g. \"CSC108H1\") and sends a JSON representation
16 | -- of the course as a response.
17 | retrieveCourse :: ServerPart Response
18 | retrieveCourse = do
19 | name <- lookText' "name"
20 | courses <- liftIO $ returnCourse name
21 | return $ createJSONResponse courses
22 |
23 | -- | Builds a list of all course codes in the database.
24 | index :: ServerPart Response
25 | index = do
26 | response <- liftIO $ runDb $ do
27 | coursesList :: [Entity Courses] <- selectList [] []
28 | let codes = map (coursesCode . entityVal) coursesList
29 | return $ T.unlines codes :: SqlPersistM T.Text
30 | return $ toResponse response
31 |
32 | -- | Returns all course info for a given department.
33 | courseInfo :: ServerPart Response
34 | courseInfo = do
35 | dept <- lookText' "dept"
36 | fmap createJSONResponse (CourseHelpers.getDeptCourses dept)
37 |
--------------------------------------------------------------------------------
/app/Controllers/Generate.hs:
--------------------------------------------------------------------------------
1 | module Controllers.Generate
2 | (generateResponse, findAndSavePrereqsResponse) where
3 |
4 | import Control.Monad ()
5 | import Control.Monad.IO.Class (liftIO)
6 | import Data.Aeson (decode, object, (.=))
7 | import Data.List (nub)
8 | import Data.Maybe (fromJust, isNothing, mapMaybe)
9 | import qualified Data.Text.Lazy as TL
10 | import Database.CourseQueries (reqsForPost, returnPost)
11 | import DynamicGraphs.GraphOptions (CourseGraphOptions (..), GraphOptions (..))
12 | import DynamicGraphs.WriteRunDot (generateAndSavePrereqResponse, getBody)
13 | import Happstack.Server
14 | import MasterTemplate
15 | import Scripts
16 | import Text.Blaze ((!))
17 | import qualified Text.Blaze.Html5 as H
18 | import qualified Text.Blaze.Html5.Attributes as A
19 | import Util.Happstack (createJSONResponse)
20 |
21 | generateResponse :: ServerPart Response
22 | generateResponse =
23 | ok $ toResponse $
24 | masterTemplate "Courseography - Generate"
25 | []
26 | (do
27 | header "generate-prerequisites"
28 | generatePrerequisites
29 | )
30 | generateScripts
31 |
32 | generatePrerequisites :: H.Html
33 | generatePrerequisites =
34 | H.html $ do
35 | H.head $
36 | H.title "Generate Prerequisites!"
37 | H.div ! A.id "generateRoot" $ ""
38 |
39 | findAndSavePrereqsResponse :: ServerPart Response
40 | findAndSavePrereqsResponse = do
41 | method PUT
42 | requestBody <- getBody
43 | let coursesOptions :: CourseGraphOptions = fromJust $ decode requestBody
44 |
45 | postResults <- liftIO $ mapM (\code -> do
46 | post <- returnPost (TL.toStrict code)
47 | return (TL.toStrict code, post))
48 | (programs coursesOptions)
49 |
50 | let invalidPrograms = map fst $ filter (isNothing . snd) postResults
51 | validPrograms = mapMaybe snd postResults
52 |
53 | allCourses <- liftIO $ nub <$>
54 | if all (== TL.empty) (courses coursesOptions)
55 | then return $ map TL.pack (concatMap reqsForPost validPrograms)
56 | else return $ courses coursesOptions
57 |
58 | let updatedCoursesOptions = coursesOptions
59 | { courses = map TL.toUpper allCourses
60 | , graphOptions = (graphOptions coursesOptions)
61 | { taken = map TL.toUpper (taken (graphOptions coursesOptions))
62 | , departments = map TL.toUpper (departments (graphOptions coursesOptions))
63 | }
64 | }
65 |
66 | if all (== TL.empty) (courses coursesOptions) && not (null invalidPrograms)
67 | then return $ createJSONResponse $ object ["error" .= object ["invalidPrograms" .= invalidPrograms]]
68 | else liftIO $ generateAndSavePrereqResponse updatedCoursesOptions
69 |
--------------------------------------------------------------------------------
/app/Controllers/Graph.hs:
--------------------------------------------------------------------------------
1 | module Controllers.Graph (graphResponse, index, getGraphJSON, graphImageResponse) where
2 |
3 | import Control.Monad.IO.Class (liftIO)
4 | import Data.Aeson (object, (.=))
5 | import Data.Maybe (fromMaybe)
6 | import Happstack.Server (Response, ServerPart, look, lookText', ok, toResponse)
7 | import MasterTemplate (header, masterTemplate)
8 | import Scripts (graphScripts)
9 | import Text.Blaze ((!))
10 | import qualified Text.Blaze.Html5 as H
11 | import qualified Text.Blaze.Html5.Attributes as A
12 |
13 | import Config (runDb)
14 | import Database.CourseQueries (getGraph)
15 | import Database.Persist.Sqlite (Entity, SelectOpt (Asc), SqlPersistM, selectList, (==.))
16 | import Database.Tables as Tables (EntityField (GraphDynamic, GraphTitle), Graph, Text)
17 | import Export.GetImages (getActiveGraphImage)
18 | import Response.Image (returnImageData)
19 | import Util.Happstack (createJSONResponse)
20 |
21 | graphResponse :: ServerPart Response
22 | graphResponse =
23 | ok $ toResponse $
24 | masterTemplate "Courseography - Graph"
25 | []
26 | (do
27 | header "graph"
28 | H.div ! A.id "container" $ ""
29 | )
30 | graphScripts
31 |
32 |
33 | index :: ServerPart Response
34 | index = liftIO $ runDb $ do
35 | graphsList :: [Entity Graph] <- selectList [GraphDynamic ==. False] [Asc GraphTitle]
36 | return $ createJSONResponse graphsList :: SqlPersistM Response
37 |
38 |
39 | -- | Looks up a graph using its title then gets the Shape, Text and Path elements
40 | -- for rendering graph (returned as JSON).
41 | getGraphJSON :: ServerPart Response
42 | getGraphJSON = do
43 | graphName <- lookText' "graphName"
44 | response <- liftIO $ getGraph graphName
45 | return $ createJSONResponse $ fromMaybe (object ["texts" .= ([] :: [Text]),
46 | "shapes" .= ([] :: [Text]),
47 | "paths" .= ([] :: [Text])]) response
48 |
49 |
50 | -- | Returns an image of the graph requested by the user, given graphInfo stored in local storage.
51 | graphImageResponse :: ServerPart Response
52 | graphImageResponse = do
53 | graphInfo <- look "JsonLocalStorageObj"
54 | (svgFilename, imageFilename) <- liftIO $ getActiveGraphImage graphInfo
55 | liftIO $ returnImageData svgFilename imageFilename
56 |
--------------------------------------------------------------------------------
/app/Css/Constants.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : Css.Constants
3 | Description : Defines the constants for the other CSS modules.
4 | -}
5 | module Css.Constants
6 | (-- * Colors
7 | theoryDark,
8 | coreDark,
9 | seDark,
10 | systemsDark,
11 | graphicsDark,
12 | dbwebDark,
13 | numDark,
14 | aiDark,
15 | hciDark,
16 | mathDark,
17 | introDark,
18 | titleColour,
19 | lightGrey,
20 | -- * Graph Styles
21 | nodeFontSize,
22 | hybridFontSize,
23 | boolFontSize,
24 | regionFontSize
25 | ) where
26 |
27 | import Data.Text as T
28 | import Prelude hiding ((**))
29 |
30 | {- Colors -}
31 |
32 | -- |Defines the color of a grayish blue.
33 | theoryDark :: T.Text
34 | theoryDark = "#B1C8D1"
35 |
36 | -- |Defines the color of a light gray.
37 | coreDark :: T.Text
38 | coreDark = "#C9C9C9"
39 |
40 | -- |Defines the color of a soft red.
41 | seDark :: T.Text
42 | seDark = "#E68080"
43 |
44 | -- |Defines the color of a light violet.
45 | systemsDark :: T.Text
46 | systemsDark = "#C285FF"
47 |
48 | -- |Defines the color of a mostly desaturated dark lime green.
49 | graphicsDark :: T.Text
50 | graphicsDark = "#66A366"
51 |
52 | -- |Defines the color of a strong pink.
53 | dbwebDark :: T.Text
54 | dbwebDark = "#C42B97"
55 |
56 | -- |Defines the color of a very light green.
57 | numDark :: T.Text
58 | numDark = "#B8FF70"
59 |
60 | -- |Defines the color of a very light blue.
61 | aiDark :: T.Text
62 | aiDark = "#80B2FF"
63 |
64 | -- |Defines the color of a soft lime green.
65 | hciDark :: T.Text
66 | hciDark = "#91F27A"
67 |
68 | -- |Defines the color of a slightly desaturated violet.
69 | mathDark :: T.Text
70 | mathDark = "#8A67BE"
71 |
72 | -- |Defines the color of a moderate cyan.
73 | introDark :: T.Text
74 | introDark = "#5DD5B8"
75 |
76 | -- |Defines the color of a very dark blue.
77 | titleColour :: T.Text
78 | titleColour = "#072D68"
79 |
80 | -- |Defines the color of a light gray.
81 | lightGrey :: T.Text
82 | lightGrey = "#CCCCCC"
83 |
84 |
85 | {- Graph styles -}
86 |
87 | -- |Defines node font size, 12 in pixels.
88 | nodeFontSize :: Num a => a
89 | nodeFontSize = 12
90 |
91 | -- |Defines hybrid font size, 7 in pixels.
92 | hybridFontSize :: Double
93 | hybridFontSize = 7
94 |
95 |
96 | -- |Defines bool font size, 6 in pixels.
97 | boolFontSize :: Num a => a
98 | boolFontSize = 6
99 |
100 | -- |Defines region font size, 14 in pixels.
101 | regionFontSize :: Num a => a
102 | regionFontSize = 13
103 |
--------------------------------------------------------------------------------
/app/Database/CourseInsertion.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : Database.CourseInsertion
3 | Description : Functions that insert/update course information in the
4 | database.
5 |
6 | This module contains a bunch of functions related to inserting information
7 | into the database. These functions are used as helpers for the WebParsing module.
8 | -}
9 |
10 | module Database.CourseInsertion
11 | (insertCourse,
12 | saveGraphJSON) where
13 |
14 | import Config (runDb)
15 | import Control.Monad.IO.Class (liftIO)
16 | import qualified Data.Aeson as Aeson
17 | import qualified Data.Text as T
18 | import Database.Persist.Class (selectKeysList)
19 | import Database.Persist.Sqlite (SqlPersistM, insert, insertMany_, insert_, selectFirst, (==.))
20 | import Database.Tables hiding (breadth, distribution, paths, shapes, texts)
21 | import Happstack.Server (Response, ServerPart, lookBS, lookText', toResponse)
22 |
23 | -- | Inserts SVG graph data into Texts, Shapes, and Paths tables
24 | saveGraphJSON :: ServerPart Response
25 | saveGraphJSON = do
26 | jsonStr <- lookBS "jsonData"
27 | nameStr <- lookText' "nameData"
28 | let jsonObj = Aeson.decode jsonStr :: Maybe SvgJSON
29 | case jsonObj of
30 | Nothing -> return $ toResponse ("Error" :: String)
31 | Just (SvgJSON texts shapes paths) -> do
32 | _ <- liftIO $ runDb $ insertGraph nameStr texts shapes paths
33 | return $ toResponse ("Success" :: String)
34 | where
35 | insertGraph :: T.Text -> [Text] -> [Shape] -> [Path] -> SqlPersistM ()
36 | insertGraph nameStr_ texts shapes paths = do
37 | gId <- insert $ Graph nameStr_ 256 256 False
38 | insertMany_ $ map (\text -> text {textGraph = gId}) texts
39 | insertMany_ $ map (\shape -> shape {shapeGraph = gId}) shapes
40 | insertMany_ $ map (\path -> path {pathGraph = gId}) paths
41 |
42 | --contains' :: PersistEntity m => T.Text -> SqlPersistM m
43 | --contains field query = Filter field (Left $ T.concat ["%", query, "%"]) (BackendSpecificFilter "LIKE")
44 |
45 | -- Get Key of correspondig record in Distribution column
46 | getDistributionKey :: T.Text -> SqlPersistM (Maybe (Key Distribution))
47 | getDistributionKey description_ = do
48 | keyListDistribution :: [Key Distribution] <- selectKeysList [ DistributionDescription ==. description_ ] []
49 | -- option: keyListDistribution :: [DistributionId] <- selectKeysList [ DistributionDescription `contains'` description] []
50 | return $ case keyListDistribution of
51 | [] -> Nothing
52 | _ -> Just (head keyListDistribution)
53 |
54 | getBreadthKey :: T.Text -> SqlPersistM (Maybe (Key Breadth))
55 | getBreadthKey description_ = do
56 | keyListBreadth :: [Key Breadth] <- selectKeysList [ BreadthDescription ==. description_ ] []
57 | -- option: selectKeysList [ BreadthDescription `contains'` description] []
58 | return $ case keyListBreadth of
59 | [] -> Nothing
60 | _ -> Just (head keyListBreadth)
61 |
62 | -- | Inserts course into the Courses table.
63 | insertCourse :: (Courses, T.Text, T.Text) -> SqlPersistM ()
64 | insertCourse (course, breadth, distribution) = do
65 | maybeCourse <- selectFirst [CoursesCode ==. coursesCode course] []
66 | breadthKey <- getBreadthKey breadth
67 | distributionKey <- getDistributionKey distribution
68 | case maybeCourse of
69 | Nothing -> insert_ $ course {coursesBreadth = breadthKey,
70 | coursesDistribution = distributionKey}
71 | Just _ -> return ()
72 |
--------------------------------------------------------------------------------
/app/Database/CourseVideoSeed.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : Database.CourseVideoSeed
3 | Description : Contains the data and functions for seeding the courseVideos.
4 | -}
5 | module Database.CourseVideoSeed
6 | (courseVideos, seedVideos) where
7 |
8 | import Config (runDb)
9 | import Data.Text (Text)
10 | import Database.Persist.Sqlite (SqlPersistM, updateWhere, (=.), (==.))
11 | import Database.Tables hiding (Text)
12 |
13 | -- | Defines the constant list of ordered pairs pertaining to the routes for
14 | -- course videos. The first Text variable in each ordered pair is the course.
15 | -- The second List of Text variable is the routes to the videos.
16 | courseVideos :: [(Text, [Text])]
17 | courseVideos = [
18 | ("CSC240H1", ["/static/videos/csc240.mp4"]),
19 | ("CSC336H1", ["/static/videos/csc336.mp4"]),
20 | ("CSC436H1", ["/static/videos/csc436.mp4"]),
21 | ("CSC438H1", ["/static/videos/csc438.mp4"]),
22 | ("CSC456H1", ["/static/videos/csc456.mp4"]),
23 | ("CSC463H1", ["/static/videos/csc463.mp4"])]
24 |
25 | seedVideo :: (Text, [Text]) -> SqlPersistM ()
26 | seedVideo (code, videos) =
27 | updateWhere [CoursesCode ==. code] [CoursesVideoUrls =. videos]
28 |
29 | -- | Sets the video routes of all course rows.
30 | seedVideos :: IO ()
31 | seedVideos = runDb $ mapM_ seedVideo courseVideos
32 |
--------------------------------------------------------------------------------
/app/Database/DataType.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE DeriveGeneric, TemplateHaskell #-}
2 |
3 | {-|
4 | Module : Database.DataType
5 | Description : Contains a single enumeration type, ShapeType.
6 |
7 | This is a small module that contains a single enumeration type.
8 | Note that we can't include this in "Database.Tables" because of
9 | a restriction on Template Haskell.
10 | -}
11 | module Database.DataType where
12 |
13 | import Data.Aeson
14 | import Database.Persist.TH
15 | import GHC.Generics
16 |
17 | -- | Defines a datatype of a shape, used in Shape json table.
18 | data ShapeType = BoolNode | Node | Hybrid | Region
19 | deriving (Show, Read, Eq, Generic)
20 |
21 | -- | Results from [derivePersistField](https://hackage.haskell.org/package/persistent-template-2.5.1.6/docs/Database-Persist-TH.html#v:derivePersistField)
22 | -- call, as does PersistField, most importantly, it allows the data type to be
23 | -- a column in the database.
24 | derivePersistField "ShapeType"
25 |
26 | -- | Results from call of [ToJSON](https://hackage.haskell.org/package/aeson-1.1.0.0/docs/Data-Aeson.html#t:ToJSON)
27 | -- .
28 | instance ToJSON ShapeType
29 |
30 | -- | Results from call of [FromJSON](https://hackage.haskell.org/package/aeson-1.1.0.0/docs/Data-Aeson.html#t:FromJSON)
31 | -- .
32 | instance FromJSON ShapeType
33 |
34 | data PostType = Specialist | Major | Minor | Focus | Certificate | Other
35 | deriving (Show, Read, Eq, Generic)
36 | derivePersistField "PostType"
37 |
38 | instance ToJSON PostType
39 | instance FromJSON PostType
40 |
--------------------------------------------------------------------------------
/app/Database/Database.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : Database.Database
3 | Description : Main module for database course seeding.
4 |
5 | The main module for parsing course information from the web and
6 | inserting it into the database. Run when @cabal run database@ is executed.
7 | -}
8 |
9 | module Database.Database
10 | (populateCalendar, setupDatabase) where
11 |
12 | import Config (databasePath, runDb)
13 | import Control.Monad.IO.Class (liftIO)
14 | import Data.Maybe (fromMaybe)
15 | import Data.Text as T (findIndex, length, reverse, take, unpack)
16 | import Database.CourseVideoSeed (seedVideos)
17 | import Database.Persist.Sqlite (insert_, runMigration)
18 | import Database.Tables
19 | import System.Directory (createDirectoryIfMissing)
20 | import WebParsing.ArtSciParser (parseCalendar)
21 |
22 |
23 | distTableSetUpStr :: String
24 | distTableSetUpStr = "Distribution table set up"
25 | breathTableSetUpStr :: String
26 | breathTableSetUpStr = "breadth table set up"
27 |
28 |
29 | -- | Creates the database if it doesn't exist and runs migrations.
30 | setupDatabase :: IO ()
31 | setupDatabase = do
32 | -- Create db folder if it doesn't exist
33 | dbPath <- liftIO databasePath
34 | let ind = (T.length dbPath -) . fromMaybe 0 . T.findIndex (=='/') . T.reverse $ dbPath
35 | db = T.unpack $ T.take ind dbPath
36 | createDirectoryIfMissing True db
37 | runDb $ runMigration migrateAll
38 |
39 | -- | Sets up the course information from Artsci Calendar
40 | populateCalendar :: IO ()
41 | populateCalendar = do
42 | populateStaticInfo
43 | parseCalendar
44 |
45 | -- | Sets up the tables and seeds the videos for the database.
46 | populateStaticInfo :: IO ()
47 | populateStaticInfo = do
48 | setupDistributionTable
49 | print distTableSetUpStr
50 | setupBreadthTable
51 | print breathTableSetUpStr
52 | seedVideos
53 |
54 | -- | Sets up the Distribution table.
55 | setupDistributionTable :: IO ()
56 | setupDistributionTable = runDb $ do
57 | insert_ $ Distribution "Humanities"
58 | insert_ $ Distribution "Social Science"
59 | insert_ $ Distribution "Science"
60 |
61 | -- | Sets up the Breadth table.
62 | setupBreadthTable :: IO ()
63 | setupBreadthTable = runDb $ do
64 | insert_ $ Breadth "Creative and Cultural Representations (1)"
65 | insert_ $ Breadth "Thought, Belief, and Behaviour (2)"
66 | insert_ $ Breadth "Society and its Institutions (3)"
67 | insert_ $ Breadth "Living Things and Their Environment (4)"
68 | insert_ $ Breadth "The Physical and Mathematical Universes (5)"
69 | insert_ $ Breadth "No Breadth"
70 |
--------------------------------------------------------------------------------
/app/Database/README.md:
--------------------------------------------------------------------------------
1 | Information
2 | ===========
3 |
4 | Summary
5 | -------
6 |
7 | Contains modules that interact with the database, as well as some data types.
8 |
9 | Notable Files
10 | -------------
11 |
12 | Tables is where the database schema is located.
13 |
--------------------------------------------------------------------------------
/app/Database/Requirement.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : Database.Requirement
3 | Description : Requirement
4 |
5 | Module containing data type that represents a "Requirement".
6 |
7 | We will use parsed data to create instances of this type.
8 | -}
9 |
10 | module Database.Requirement (
11 | Req (..),
12 | Modifier (..)
13 | ) where
14 |
15 | data Req = None
16 | | J String String
17 | | ReqAnd [Req]
18 | | ReqOr [Req]
19 | | Fces Float Modifier
20 | | Grade String Req
21 | | Gpa Float String
22 | | Program String
23 | | Raw String deriving (Eq, Show)
24 |
25 | data Modifier = Department String
26 | | Level String
27 | | ModAnd [Modifier]
28 | | ModOr [Modifier]
29 | | Requirement Req deriving (Eq, Show)
30 |
--------------------------------------------------------------------------------
/app/DynamicGraphs/CourseFinder.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : DynamicGraphs.CourseFinder
3 | Description : Retrieve course information needed to generate graphs.
4 |
5 | This module contains the logic requests for information needed to
6 | generate graphs before retrieving and structuring the necessary data.
7 | -}
8 | module DynamicGraphs.CourseFinder (lookupCourses) where
9 |
10 | import Control.Monad.Trans.Class (lift)
11 | import Control.Monad.Trans.State (StateT, execStateT, modify)
12 | import qualified Data.Map.Strict as Map
13 | import qualified Data.Set as Set
14 | import qualified Data.Text.Lazy as T
15 | import Database.CourseQueries (prereqsForCourse)
16 | import Database.Requirement (Modifier (..), Req (..))
17 | import DynamicGraphs.GraphOptions (GraphOptions (..))
18 | import WebParsing.ReqParser (parseReqs)
19 |
20 | lookupCourses :: GraphOptions -> [T.Text] -> IO (Map.Map T.Text Req)
21 | lookupCourses options courses =
22 | execStateT (mapM_ (lookupCourse options) courses) Map.empty
23 |
24 | lookupCourse :: GraphOptions -> T.Text -> StateT (Map.Map T.Text Req) IO ()
25 | lookupCourse options code = do
26 | prereqResults <- lift $ prereqsForCourse $ T.toStrict code
27 | case prereqResults of
28 | Left _ -> return ()
29 | Right (courseCode, prereqStr) -> do
30 | let prereqs = parseReqs (T.unpack $ T.fromStrict prereqStr)
31 | modify $ Map.insert (T.fromStrict courseCode) prereqs
32 | lookupReqs options prereqs
33 |
34 | lookupReqs :: GraphOptions -> Req -> StateT (Map.Map T.Text Req) IO ()
35 | lookupReqs options (J name _) = do
36 | if Set.member name (Set.fromList $ map T.unpack (taken options)) ||
37 | not (Set.member (take 3 name) $ Set.fromList $ map T.unpack (departments options))
38 | -- This course has been taken or is not a department we want to include; we don't need its prerequisites
39 | then return ()
40 | else lookupCourse options $ T.pack name
41 | lookupReqs options (ReqAnd parents) = mapM_ (lookupReqs options) parents
42 | lookupReqs options (ReqOr parents) =
43 | if any hasTaken parents
44 | -- We've taken at least one of parents, so this entire ReqOr is satisfied
45 | then return ()
46 | else mapM_ (lookupReqs options) parents
47 | where
48 | hasTaken :: Req -> Bool
49 | hasTaken (J name _) = Set.member name (Set.fromList $ map T.unpack (taken options))
50 | hasTaken _ = False
51 | lookupReqs options (Fces _ (Requirement parent)) = lookupReqs options parent
52 | lookupReqs options (Grade _ parent) = lookupReqs options parent
53 | -- This will catch None, Raw, and Fces with non-course modifiers
54 | lookupReqs _ _ = return ()
55 |
--------------------------------------------------------------------------------
/app/DynamicGraphs/GraphNodeUtils.hs:
--------------------------------------------------------------------------------
1 | module DynamicGraphs.GraphNodeUtils
2 | ( stringifyModAnd
3 | , formatModOr
4 | , concatModOr
5 | , maybeHead
6 | , paddingSpaces
7 | ) where
8 |
9 | import Database.Requirement (Modifier (..), Req (..))
10 |
11 | -- | Converts the given number of credits and list of modifiers into a string
12 | -- | in readable English
13 | -- | Assumes each modifier constructor appears in modifiers at most once
14 | -- | The ModOr constructor may appear more than once, but each occurrence
15 | -- | of ModOr contains exactly one constructor for all its elements
16 | -- | and such constructor does not appear anywhere else in ModAnd
17 | stringifyModAnd :: Float -> [Modifier] -> String
18 | stringifyModAnd creds modifiers = let
19 | dept = maybeHead [x | Department x <- modifiers]
20 | deptFormatted = case dept of
21 | Nothing -> ""
22 | Just x -> ' ' : x
23 | depts = maybeHead [xs | ModOr xs@((Department _):_) <- modifiers]
24 | deptsFormatted = case depts of
25 | Nothing -> ""
26 | Just xs -> ' ' : concatModOr xs
27 | level = maybeHead [x | Level x <- modifiers]
28 | levelFormatted = case level of
29 | Nothing -> ""
30 | Just x -> " at the " ++ x ++ " level"
31 | levels = maybeHead [xs | ModOr xs@((Level _):_) <- modifiers]
32 | levelsFormatted = case levels of
33 | Nothing -> ""
34 | Just xs -> " at the " ++ concatModOr xs ++ " level"
35 | raw = maybeHead [x | Requirement (Raw x) <- modifiers]
36 | rawFormatted = case raw of
37 | Nothing -> ""
38 | Just x -> " from " ++ x
39 | raws = maybeHead [xs | ModOr xs@((Requirement (Raw _)):_) <- modifiers]
40 | rawsFormatted = case raws of
41 | Nothing -> ""
42 | Just xs -> " from " ++ concatModOr xs
43 |
44 | in show creds ++ deptFormatted ++ deptsFormatted ++ " FCEs"
45 | ++ levelFormatted ++ levelsFormatted ++ rawFormatted ++ rawsFormatted
46 |
47 | -- | Formats a ModOr into Fces string
48 | -- | Assumes all modifiers in the ModOr have the same constructor
49 | formatModOr :: Float -> [Modifier] -> String
50 | formatModOr creds mods@((Department _):_) = show creds ++ " " ++ concatModOr mods ++ " FCEs"
51 | formatModOr creds mods@((Level _):_) = show creds ++ " FCEs at the " ++ concatModOr mods ++ " level"
52 | formatModOr creds mods@((Requirement (Raw _)):_) = show creds ++ " FCEs from " ++ concatModOr mods
53 | formatModOr _ _ = "" -- we should never get here
54 |
55 | -- | Joins a list of modifiers in ModOr together with a "/" or "or"
56 | -- | Assumes all modifiers in the list have the same constructor
57 | concatModOr :: [Modifier] -> String
58 | concatModOr [Level x] = x
59 | concatModOr ((Level x):xs) = x ++ "/" ++ concatModOr xs
60 | concatModOr [Department x] = x
61 | concatModOr ((Department x):xs) = x ++ "/" ++ concatModOr xs
62 | concatModOr [Requirement (Raw x)] = x
63 | concatModOr ((Requirement (Raw x)):xs) = x ++ " or " ++ concatModOr xs
64 | concatModOr _ = "" -- we should never get here
65 |
66 | -- | Returns Just the first element of the given list, or Nothing if the list is empty
67 | maybeHead :: [a] -> Maybe a
68 | maybeHead [] = Nothing
69 | maybeHead (x:_) = Just x
70 |
71 | paddingSpaces :: Int -> [Char]
72 | paddingSpaces n = Prelude.replicate n ' '
73 |
--------------------------------------------------------------------------------
/app/DynamicGraphs/GraphOptions.hs:
--------------------------------------------------------------------------------
1 | module DynamicGraphs.GraphOptions where
2 |
3 | import Data.Aeson (FromJSON (parseJSON), withObject, (.!=), (.:?))
4 | import qualified Data.Text.Lazy as T
5 |
6 | data GraphOptions =
7 | GraphOptions { taken :: [T.Text], -- courses to exclude from graph
8 | departments :: [T.Text], -- department prefixes to include
9 | excludedDepth :: Int, -- depth to recurse on courses from excluded departments
10 | maxDepth :: Int, -- total recursive depth to recurse on (depth of the overall graph)
11 | courseNumPrefix :: [Int], -- filter based on course number (most useful for filtering based on year)
12 | distribution :: [T.Text], -- distribution to include: like "artsci", or "engineering"
13 | location :: [T.Text], -- location of courses to include: like "utsg", or "utsc"
14 | includeRaws :: Bool, -- True to include nodes which are raw values
15 | includeGrades :: Bool -- True to include grade nodes
16 | } deriving (Show)
17 |
18 | data CourseGraphOptions = CourseGraphOptions { courses :: [T.Text], programs :: [T.Text], graphOptions :: GraphOptions }
19 | deriving (Show)
20 |
21 | defaultGraphOptions :: GraphOptions
22 | defaultGraphOptions =
23 | GraphOptions [] -- taken
24 | [] -- departments
25 | 0 -- excludedDepth
26 | (-1) -- maxDepth
27 | [] -- courseNumPrefix
28 | [] -- distribution
29 | [] -- location
30 | True -- includeRaws
31 | True -- includeGrades
32 |
33 | instance FromJSON CourseGraphOptions where
34 | parseJSON = withObject "Expected Object for GraphOptions" $ \o -> do
35 | rootCourses <- o .:? "courses" .!= []
36 | rootPrograms <- o.:? "programs" .!= []
37 | takenCourses <- o .:? "taken" .!= []
38 | dept <- o .:? "departments" .!= []
39 | excludedCourseDepth <- o .:? "excludedDepth" .!= 0
40 | maxGraphDepth <- o .:? "maxDepth" .!= (-1)
41 | courseNumPref <- o .:? "courseNumPrefix" .!= []
42 | distrib <- o .:? "distribution" .!= []
43 | includedLocation <- o .:? "location" .!= []
44 | incRaws <- o .:? "includeRaws" .!= True
45 | incGrades <- o .:? "includeGrades" .!= True
46 | let options = GraphOptions takenCourses
47 | dept
48 | excludedCourseDepth
49 | maxGraphDepth
50 | courseNumPref
51 | distrib
52 | includedLocation
53 | incRaws
54 | incGrades
55 | return $ CourseGraphOptions rootCourses rootPrograms options
56 |
--------------------------------------------------------------------------------
/app/DynamicGraphs/WriteRunDot.hs:
--------------------------------------------------------------------------------
1 | module DynamicGraphs.WriteRunDot where
2 |
3 | import Control.Monad (forM_)
4 | import Control.Monad.IO.Class (liftIO)
5 | import qualified Data.ByteString as B
6 | import qualified Data.ByteString.Lazy as L
7 | import Data.GraphViz hiding (Str)
8 | import Data.Hash.MD5 (Str (Str), md5s)
9 | import Data.List (sort)
10 | import Data.Maybe (fromMaybe)
11 | import qualified Data.Text as T
12 | import Data.Text.Encoding (decodeUtf8)
13 | import Database.CourseQueries (getGraph)
14 | import DynamicGraphs.GraphGenerator (coursesToPrereqGraph, coursesToPrereqGraphExcluding,
15 | graphProfileHash)
16 | import DynamicGraphs.GraphOptions (CourseGraphOptions (..), GraphOptions (..))
17 | import Happstack.Server (ServerPart, askRq)
18 | import Happstack.Server.SimpleHTTP (Response)
19 | import Happstack.Server.Types (takeRequestBody, unBody)
20 | import Svg.Parser (parseDynamicSvg)
21 | import System.Directory (createDirectoryIfMissing)
22 | import System.FilePath (combine, normalise)
23 | import Util.Happstack (createJSONResponse)
24 |
25 | doDots :: PrintDotRepr dg n => [(FilePath, dg n)] -> IO ()
26 | doDots cases = do
27 | forM_ cases createImage
28 | putStrLn "Look in graphs/gen to see the created graphs"
29 |
30 | generatePrereqsForCourses :: (FilePath, [String]) -> IO ()
31 | generatePrereqsForCourses (output, rootCourses) = do
32 | graph <- coursesToPrereqGraph rootCourses
33 | _ <- createImage (output, graph)
34 | putStrLn $ "Generated prerequisite graph for "
35 | ++ show rootCourses
36 | ++ " in graphs/gen"
37 |
38 | getBody :: ServerPart L.ByteString
39 | getBody = do
40 | req <- askRq
41 | body <- liftIO $ takeRequestBody req
42 | case body of
43 | Just rqbody -> return . unBody $ rqbody
44 | Nothing -> return ""
45 |
46 | generateAndSavePrereqResponse :: CourseGraphOptions -> IO Response
47 | generateAndSavePrereqResponse coursesOptions = do
48 | cached <- getGraph graphHash
49 | case cached of
50 | Just cachedGraph -> return $ createJSONResponse cachedGraph
51 | Nothing -> do
52 | graph <- coursesToPrereqGraphExcluding (courses coursesOptions) (graphOptions coursesOptions)
53 | bString <- graphToByteString graph
54 | -- Parse the generated SVG and store it in the database.
55 | parseDynamicSvg graphHash $ decodeUtf8 bString
56 | storedGraph <- getGraph graphHash
57 | return $ createJSONResponse $ fromMaybe graphNotFound storedGraph
58 | where
59 | graphHash :: T.Text
60 | graphHash = hash coursesOptions
61 | graphNotFound = error "Graph should have been generated but was not found"
62 |
63 | -- | Hash function to uniquely identify the graph layout.
64 | hash :: CourseGraphOptions -> T.Text
65 | hash coursesOptions = hashFunction (key, graphProfileHash)
66 | where key = coursesOptions {
67 | courses = sort $ courses coursesOptions,
68 | graphOptions = options {
69 | taken = sort $ taken options,
70 | departments = sort $ departments options,
71 | distribution = sort $ distribution options,
72 | location = sort $ location options,
73 | courseNumPrefix = sort $ courseNumPrefix options
74 | }
75 | }
76 | where options = graphOptions coursesOptions
77 | hashFunction :: (CourseGraphOptions, String) -> T.Text
78 | hashFunction = T.pack . ("graph_" ++) . md5s . Str . show
79 |
80 | graphToByteString :: PrintDotRepr dg n => dg n -> IO B.ByteString
81 | graphToByteString graph = graphvizWithHandle Dot graph Svg B.hGetContents
82 |
83 | createImage :: PrintDotRepr dg n => (FilePath, dg n) -> IO FilePath
84 | createImage (n, g) = do
85 | _ <- createDirectoryIfMissing False filepath
86 | createImageInDir filepath n Svg g
87 | where
88 | filepath = normalise "graphs/gen"
89 |
90 | -- | Here runGraphvizCommand Dot creates the final graph given the input DotGraph object g and connects it
91 | -- with the file path(by combining directory d and filename n) to make the final graph in the required directory.
92 | createImageInDir :: PrintDotRepr dg n
93 | => FilePath -- ^ directory to write in
94 | -> FilePath -- ^ filename
95 | -> GraphvizOutput -- ^ filetype to write in
96 | -> dg n -- ^ graph to draw
97 | -> IO FilePath
98 | createImageInDir d n o g = Data.GraphViz.addExtension (runGraphvizCommand Dot g) o (combine d n)
99 |
--------------------------------------------------------------------------------
/app/Export/ImageConversion.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : Export.ImageConversion
3 | Description : Includes functions for converting SVG files to PNG.
4 | -}
5 | module Export.ImageConversion
6 | (createImageFile) where
7 |
8 | import GHC.IO.Handle.Types
9 | import System.Process (ProcessHandle, createProcess, shell, waitForProcess)
10 |
11 | -- | Opens a new process to convert an SVG (inName) to a PNG (outName)
12 | -- Note: hGetContents can be used to read Handles. Useful when trying to read from
13 | -- stdout.
14 | createImageFile :: String -> String -> IO ()
15 | createImageFile inName outName = do
16 | (_, _, _, pid) <- convertToImage inName outName
17 | putStrLn "Waiting for process..."
18 | _ <- waitForProcess pid
19 | putStrLn "Process Complete"
20 |
21 | -- | Converts an SVG file to a PNG file. Note that ImageMagick's 'magick' command
22 | -- can take in file descriptors.
23 | convertToImage :: String -> String -> IO
24 | (Maybe Handle,
25 | Maybe Handle,
26 | Maybe Handle,
27 | ProcessHandle)
28 | convertToImage inName outName =
29 | createProcess $ shell $ "magick " ++ inName ++ " " ++ outName
30 |
--------------------------------------------------------------------------------
/app/Export/LatexGenerator.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : Export.LatexGenerator
3 | Description : Contains functions for creating LaTeX text.
4 | -}
5 | module Export.LatexGenerator
6 | (generateTex) where
7 |
8 | import Text.LaTeX
9 | import Text.LaTeX.Packages.Fancyhdr
10 | import Text.LaTeX.Packages.Geometry
11 | import Text.LaTeX.Packages.Graphicx
12 |
13 | -- | Create a TEX file named texName that includes all of the images in
14 | -- imageNames
15 | generateTex :: [String] -> String -> IO ()
16 | generateTex imageNames texName = execLaTeXT (buildTex imageNames) >>= renderFile texName
17 |
18 | -- | Combine the preamble and the document text into a single block of latex
19 | -- code. The document text contains code to insert all of the images in
20 | -- imageNames.
21 | buildTex :: Monad m => [String] -> LaTeXT_ m
22 | buildTex imageNames = do
23 | preamble
24 | document (body imageNames)
25 |
26 | -- | Defines documentclass and packages used.
27 | preamble :: Monad m => LaTeXT_ m
28 | preamble = do
29 | documentclass [] article
30 | usepackage [] graphicx
31 | usepackage [] geometry
32 | applyGeometry [GLandscape True, GWidth (In 9)]
33 | let mySettings = defaultHdrSettings {leftHeader = "Graph and Timetables", rightHeader = "courseography.cdf.toronto.edu"}
34 | applyHdrSettings mySettings
35 | raw "\\pagenumbering{gobble}"
36 |
37 | -- | Adds an includegraphics command for each image in imageNames. If an empty
38 | -- list of imageNames was provided, the body will be empty.
39 | body :: Monad m => [String] -> LaTeXT_ m
40 | body [] = ""
41 | body (imageName:imageNames) = do
42 | center $ includegraphics [IGWidth (CustomMeasure linewidth)] imageName
43 | body imageNames
44 |
--------------------------------------------------------------------------------
/app/Export/PdfGenerator.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : Export.PdfGenerator
3 | Description : Contains functions for generating a PDF from LaTeX text.
4 | -}
5 | module Export.PdfGenerator
6 | (createPDF) where
7 |
8 | import Data.List.Utils (replace)
9 | import GHC.IO.Handle.Types
10 | import System.Directory (removeFile)
11 | import System.Process (ProcessHandle, createProcess, shell, waitForProcess)
12 |
13 | -- | Opens a new process to create a PDF from a TEX (texName) and deletes
14 | -- the tex file and extra files created by pdflatex
15 | createPDF :: String -> IO ()
16 | createPDF texName = do
17 | (_, _, _, pid) <- convertTexToPDF texName
18 | putStrLn "Waiting for a process..."
19 | _ <- waitForProcess pid
20 | let auxFile = replace ".tex" ".aux" texName
21 | logFile = replace ".tex" ".log" texName
22 | mapM_ removeFile [auxFile, logFile, texName]
23 | putStrLn "Process Complete"
24 |
25 | -- | Create a process to use the pdflatex program to create a PDF from a TEX
26 | -- file (texName). The process is run in nonstop mode and so it will not block
27 | -- if an error occurs. The resulting PDF will have the same filename as texName.
28 | convertTexToPDF :: String -> IO
29 | (Maybe Handle,
30 | Maybe Handle,
31 | Maybe Handle,
32 | ProcessHandle)
33 | convertTexToPDF texName =
34 | createProcess $ shell $ "pdflatex -interaction=nonstopmode " ++ texName
35 |
--------------------------------------------------------------------------------
/app/Export/README.md:
--------------------------------------------------------------------------------
1 | Information
2 | ===========
3 |
4 | Summary
5 | -------
6 | Includes files for exporting graphs and timetables, effectively containing
7 | all of the code used when the "export" button is pressed.
8 |
--------------------------------------------------------------------------------
/app/Export/TimetableImageCreator.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : Export.TimetableImageCreator
3 | Description : Primarily defines a function used to render SVGs with times.
4 | -}
5 | module Export.TimetableImageCreator
6 | (renderTable, renderTableHelper, times) where
7 |
8 | import Data.List (intersperse)
9 | import qualified Data.Text as T
10 | import Diagrams.Backend.SVG
11 | import Diagrams.Prelude
12 |
13 | days :: [T.Text]
14 | days = ["Mon", "Tue", "Wed", "Thu", "Fri"]
15 |
16 | -- |A list of lists of Texts, which has the "times" from 8:00 to 12:00, and
17 | -- 1:00 to 8:00.times
18 | times :: [[T.Text]]
19 | times = map (\x -> [T.pack (show x ++ ":00")]) ([8..12] ++ [1..8] :: [Int])
20 |
21 | blue3 :: Colour Double
22 | blue3 = sRGB24read "#437699"
23 |
24 | pink1 :: Colour Double
25 | pink1 = sRGB24read "#DB94B8"
26 |
27 | pomegranate :: Colour Double
28 | pomegranate = sRGB24read "#F20C00"
29 |
30 | cellWidth :: Double
31 | cellWidth = 2
32 |
33 | timeCellWidth :: Double
34 | timeCellWidth = 1.2
35 |
36 | cellHeight :: Double
37 | cellHeight = 0.4
38 |
39 | cellPaddingHeight :: Double
40 | cellPaddingHeight = 0.1
41 |
42 | fs :: Double
43 | fs = 14
44 |
45 | cell :: Diagram B
46 | cell = rect cellWidth cellHeight
47 |
48 | cellPadding :: Diagram B
49 | cellPadding = rect cellWidth cellPaddingHeight
50 |
51 | timeCell :: Diagram B
52 | timeCell = rect timeCellWidth cellHeight # lw none
53 |
54 | timeCellPadding :: Diagram B
55 | timeCellPadding = rect timeCellWidth cellPaddingHeight # lw none
56 |
57 | cellText :: T.Text -> Diagram B
58 | cellText s = font "Trebuchet MS" $ text (T.unpack s) # fontSizeO (1024/900 * fs)
59 |
60 | -- | Creates and accumulates cells according to the number of course.
61 | makeCell :: Int -> [T.Text] -> Diagram B
62 | makeCell maxCourse sList =
63 | let actualCourse = length sList
64 | emptyCellNum = if maxCourse == 0 then 1 else maxCourse - actualCourse
65 | extraCell = replicate emptyCellNum [cellPadding # fc white # lc white, cellText "" # fc white <> cell # fc white # lc white]
66 | in vsep 0.030 $
67 | concat $ map (\x -> [cellPadding # fc background # lc background, cellText x # fc white <> cell # fc background # lc background]) sList ++ extraCell
68 | where
69 | background = getBackground sList
70 |
71 | getBackground :: [T.Text] -> Colour Double
72 | getBackground s
73 | | null s = white
74 | | length s == 1 = blue3
75 | | otherwise = pomegranate
76 |
77 | header :: T.Text -> Diagram B
78 | header session = hcat (makeSessionCell session : map makeHeaderCell days) # centerX === headerBorder
79 |
80 | makeSessionCell :: T.Text -> Diagram B
81 | makeSessionCell s =
82 | timeCellPadding === (cellText s <> timeCell)
83 |
84 | makeHeaderCell :: T.Text -> Diagram B
85 | makeHeaderCell s =
86 | (cellPadding # lw none # fc white # lc white) === (cellText s <> cell # lw none)
87 |
88 | makeTimeCell :: T.Text -> Diagram B
89 | makeTimeCell s =
90 | timeCellPadding === (cellText s <> timeCell)
91 |
92 | makeRow :: [[T.Text]] -> Diagram B
93 | makeRow ([x]:xs) =
94 | let maxCourse = maximum (map length xs)
95 | in (# centerX) . hcat $
96 | makeTimeCell x : map (makeCell maxCourse) xs
97 | makeRow _ = error "invalid timetable format"
98 |
99 | headerBorder :: Diagram B
100 | headerBorder = hrule 11.2 # lw medium # lc pink1
101 |
102 | rowBorder :: Diagram B
103 | rowBorder = hrule 11.2 # lw thin # lc pink1
104 |
105 | makeTable :: [[[T.Text]]] -> T.Text -> Diagram B
106 | makeTable s session = vsep 0.04 $ header session: intersperse rowBorder (map makeRow s)
107 |
108 | -- |Creates a timetable by zipping the time and course tables.
109 | renderTable :: String -> T.Text -> T.Text -> IO ()
110 | renderTable filename courses session = do
111 | let courseTable = partition5 $ map (\x -> [x | not (T.null x)]) $ T.splitOn "_" courses
112 | renderTableHelper filename (zipWith (:) times courseTable) session
113 | where
114 | partition5 [] = []
115 | partition5 lst = take 5 lst : partition5 (drop 5 lst)
116 |
117 | -- |Renders an SVG with a width of 1024, though the documentation doesn't
118 | -- specify the units, it is assumed that these are pixels.
119 | renderTableHelper :: String -> [[[T.Text]]] -> T.Text -> IO ()
120 | renderTableHelper filename schedule session = do
121 | let g = makeTable schedule session
122 | renderSVG filename (mkWidth 1024) g
123 |
--------------------------------------------------------------------------------
/app/Main.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Description: Entry point for Courseography.
3 |
4 | This module implements a 'main' method which takes a command-line
5 | argument and executes the corresponding IO action.
6 |
7 | If no argument is provided, the default action is 'server', which
8 | starts the server.
9 | -}
10 | module Main where
11 |
12 | import Data.List (intercalate)
13 | import qualified Data.Map.Strict as Map
14 | import Data.Maybe (fromMaybe)
15 | import System.Environment (getArgs)
16 | import System.IO (hPutStrLn, stderr)
17 |
18 | -- internal dependencies
19 | import Database.Database (populateCalendar, setupDatabase)
20 | import Server (runServer)
21 | import Svg.Parser (parsePrebuiltSvgs)
22 | import Util.Documentation (generateDocs)
23 | import WebParsing.UtsgJsonParser (parseTimetable)
24 |
25 | -- dynamic graph generation
26 | import DynamicGraphs.WriteRunDot (generatePrereqsForCourses)
27 |
28 | -- | A map of command-line arguments to their corresponding IO actions.
29 | taskMap :: Map.Map String ([String] -> IO ())
30 | taskMap = Map.fromList [
31 | ("server", const runServer),
32 | ("database-calendar", const populateCalendar),
33 | ("database-timetable", const parseTimetable),
34 | ("database-graphs", const parsePrebuiltSvgs),
35 | ("docs", const generateDocs),
36 | ("generate", generate),
37 | ("database-setup", const setupDatabase)]
38 |
39 | -- | Courseography entry point.
40 | main :: IO ()
41 | main = do
42 | args <- getArgs
43 | let (taskName, rest) =
44 | case args of
45 | [] -> ("server", [])
46 | (command : remaining) -> (command, remaining)
47 | fromMaybe putUsage (Map.lookup taskName taskMap) rest
48 |
49 |
50 | -- | Print usage message to user (when main gets an incorrect argument).
51 | putUsage :: [String] -> IO ()
52 | putUsage _ = hPutStrLn stderr usageMsg
53 | where
54 | taskNames = Map.keys taskMap
55 | usageMsg = "Unrecognized argument. Available arguments:\n" ++
56 | intercalate "\n" taskNames
57 |
58 | generate :: [String] -> IO ()
59 | generate (name : courses) = generatePrereqsForCourses (name, courses)
60 | generate _ = hPutStrLn
61 | stderr
62 | "Generate Usage: generate ..."
63 |
--------------------------------------------------------------------------------
/app/MasterTemplate.hs:
--------------------------------------------------------------------------------
1 | module MasterTemplate
2 | (masterTemplate, header) where
3 |
4 | import qualified Data.Text as T
5 | import Text.Blaze ((!))
6 | import qualified Text.Blaze.Html5 as H
7 | import qualified Text.Blaze.Html5.Attributes as A
8 | import Text.Blaze.Internal (textValue)
9 | import Util.Blaze
10 |
11 |
12 | masterTemplate :: T.Text -> [H.Html] -> H.Html -> H.Html -> H.Html
13 | masterTemplate title headers body scripts =
14 | H.docTypeHtml $ do
15 | H.head $ do
16 | H.meta ! A.httpEquiv "Content-Type"
17 | ! A.content "text/html;charset=utf-8"
18 | H.title (H.toHtml title)
19 | H.link ! A.rel "icon" ! A.type_ "image/png"
20 | ! A.href "/static/res/ico/favicon.png"
21 | sequence_ headers
22 | toStylesheet "/static/style/app.css"
23 | H.body $ do
24 | body
25 | scripts
26 |
27 | -- Insert the header of the Grid and Graph. This contains the year of the timetable, and
28 | -- a link back to the Graph.
29 | header :: T.Text -> H.Html
30 | header page =
31 | H.nav ! A.class_ "row header" $ do
32 | H.div ! A.class_ "nav-left" $ do
33 | H.a ! A.href "/graph" $ do
34 | H.img ! A.id "courseography-header" ! A.src "/static/res/img/logo.png"
35 | ! H.customAttribute "context" (textValue page)
36 | H.div ! A.class_ "nav-middle" $ do
37 | H.ul ! A.id "nav-links" $ do
38 | if page == "graph"
39 | then H.li ! A.id "nav-graph" ! A.class_ "selected-page" $ toLink "/graph" "Graph"
40 | else H.li ! A.id "nav-graph" $ toLink "/graph" "Graph"
41 | if page == "grid"
42 | then H.li ! A.class_ "selected-page" $ toLink "/grid" "Grid"
43 | else H.li $ toLink "/grid" "Grid"
44 | if page == "generate-prerequisites"
45 | then H.li ! A.class_ "selected-page" $ toLink "/generate" "Generate (beta)"
46 | else H.li ! A.id "nav-generate" $ toLink "/generate" "Generate (beta)"
47 | -- H.li $ toLink "/timesearch" "Search"
48 | -- H.li $ toLink "/draw" "Draw"
49 | -- TODO: re-enable after handling new first-year courses
50 | -- H.li $ toLink "post" "Check My POSt!"
51 | if page == "about"
52 | then H.li ! A.class_ "selected-page" $ toLink "/about" "About"
53 | else H.li $ toLink "/about" "About"
54 | H.div ! A.class_ "nav-right" $ do
55 | if page `elem` ["graph", "grid"]
56 | then H.button ! A.id "nav-export" $ do
57 | H.img ! A.src "/static/res/ico/export.png" ! A.alt "Export"
58 | else ""
59 |
--------------------------------------------------------------------------------
/app/Models/Course.hs:
--------------------------------------------------------------------------------
1 | module Models.Course
2 | (buildCourse,
3 | buildMeetTimes,
4 | returnCourse) where
5 |
6 | import Config (runDb)
7 | import qualified Data.Text as T (Text, append, filter, take, toUpper)
8 | import Database.Persist.Sqlite (Entity, SqlPersistM, entityKey, entityVal, get, selectFirst,
9 | selectList, (<-.), (==.))
10 | import Database.Tables as Tables
11 |
12 | -- | Queries the database for all matching lectures, tutorials,
13 | meetingQuery :: [T.Text] -> SqlPersistM [MeetTime']
14 | meetingQuery meetingCodes = do
15 | allMeetings <- selectList [MeetingCode <-. map (T.take 6) meetingCodes] []
16 | mapM buildMeetTimes allMeetings
17 |
18 | -- | Queries the database for all information about @course@,
19 | -- constructs and returns a Course value.
20 | returnCourse :: T.Text -> IO (Maybe Course)
21 | returnCourse lowerStr = runDb $ do
22 | let courseStr = T.toUpper lowerStr
23 | -- TODO: require the client to pass the full course code
24 | let fullCodes = [courseStr, T.append courseStr "H1", T.append courseStr "Y1"]
25 | sqlCourse :: (Maybe (Entity Courses)) <- selectFirst [CoursesCode <-. fullCodes] []
26 | case sqlCourse of
27 | Nothing -> return Nothing
28 | Just course -> do
29 | meetings <- meetingQuery fullCodes
30 | Just <$> buildCourse meetings
31 | (entityVal course)
32 |
33 | -- | Queries the database for the breadth description
34 | getDescriptionB :: Maybe (Key Breadth) -> SqlPersistM (Maybe T.Text)
35 | getDescriptionB Nothing = return Nothing
36 | getDescriptionB (Just key) = do
37 | maybeBreadth <- get key
38 | return $ fmap breadthDescription maybeBreadth
39 |
40 | -- | Queries the database for the distribution description
41 | getDescriptionD :: Maybe (Key Distribution) -> SqlPersistM (Maybe T.Text)
42 | getDescriptionD Nothing = return Nothing
43 | getDescriptionD (Just key) = do
44 | maybeDistribution <- get key
45 | return $ fmap distributionDescription maybeDistribution
46 |
47 | -- | Builds a Course structure from a tuple from the Courses table.
48 | -- Some fields still need to be added in.
49 | buildCourse :: [MeetTime'] -> Courses -> SqlPersistM Course
50 | buildCourse allMeetings course = do
51 | cBreadth <- getDescriptionB (coursesBreadth course)
52 | cDistribution <- getDescriptionD (coursesDistribution course)
53 | return $ Course cBreadth
54 | -- TODO: Remove the filter and allow double-quotes
55 | (fmap (T.filter (/='\"')) (coursesDescription course))
56 | (fmap (T.filter (/='\"')) (coursesTitle course))
57 | (coursesPrereqString course)
58 | (Just allMeetings)
59 | (coursesCode course)
60 | (coursesExclusions course)
61 | cDistribution
62 | (coursesCoreqs course)
63 | (coursesVideoUrls course)
64 |
65 | -- | Queries the database for all times corresponding to a given meeting.
66 | buildMeetTimes :: Entity Meeting -> SqlPersistM Tables.MeetTime'
67 | buildMeetTimes meet = do
68 | allTimes :: [Entity Times] <- selectList [TimesMeeting ==. entityKey meet] []
69 | parsedTime <- mapM (buildTime . entityVal) allTimes
70 | return $ Tables.MeetTime' (entityVal meet) parsedTime
71 |
--------------------------------------------------------------------------------
/app/Response.hs:
--------------------------------------------------------------------------------
1 | module Response (module X) where
2 |
3 | import Response.About as X
4 | import Response.Draw as X
5 | import Response.Image as X
6 | import Response.Loading as X
7 | import Response.NotFound as X
8 |
--------------------------------------------------------------------------------
/app/Response/About.hs:
--------------------------------------------------------------------------------
1 | module Response.About
2 | (aboutResponse) where
3 |
4 | import Happstack.Server
5 | import MasterTemplate
6 | import Scripts (aboutScripts)
7 | import Text.Blaze ((!))
8 | import qualified Text.Blaze.Html5 as H
9 | import qualified Text.Blaze.Html5.Attributes as A
10 |
11 | aboutResponse :: ServerPart Response
12 | aboutResponse =
13 | ok $ toResponse $
14 | masterTemplate "Courseography - About"
15 | []
16 | (do
17 | header "about"
18 | H.div ! A.id "aboutDiv" $ ""
19 | )
20 | aboutScripts
21 |
--------------------------------------------------------------------------------
/app/Response/Draw.hs:
--------------------------------------------------------------------------------
1 | module Response.Draw
2 | (drawResponse) where
3 |
4 | import Control.Monad (forM_)
5 | import Happstack.Server
6 | import MasterTemplate
7 | import Scripts
8 | import Text.Blaze ((!))
9 | import qualified Text.Blaze.Html5 as H
10 | import qualified Text.Blaze.Html5.Attributes as A
11 |
12 | drawResponse :: ServerPart Response
13 | drawResponse =
14 | ok $ toResponse $
15 | masterTemplate "Courseography - Draw!"
16 | []
17 | (do
18 | header "draw"
19 | drawContent
20 | modePanel
21 | H.div ! A.id "react-graph" $ ""
22 | )
23 | drawScripts
24 |
25 | drawContent :: H.Html
26 | drawContent = H.div ! A.id "about-div" $ "Draw a Graph"
27 |
28 | modePanel :: H.Html
29 | modePanel = H.div ! A.id "side-panel-wrap" $ do
30 | H.div ! A.id "node-mode" ! A.class_ "mode clicked" $ "NODE (n)"
31 | H.input ! A.id "course-code"
32 | ! A.class_ "course-code"
33 | ! A.name "course-code"
34 | ! A.placeholder "Course Code"
35 | ! A.autocomplete "off"
36 | ! A.type_ "text"
37 | ! A.size "10"
38 | H.div ! A.id "add-text" ! A.class_ "button" $ "ADD"
39 | H.div ! A.id "path-mode" ! A.class_ "mode" $ "PATH (p)"
40 | H.div ! A.id "region-mode" ! A.class_ "mode" $ "REGION (r)"
41 | H.div ! A.id "finish-region" ! A.class_ "button" $ "finish (f)"
42 | H.div ! A.id "change-mode" ! A.class_ "mode" $ "SELECT/MOVE (m)"
43 | H.div ! A.id "erase-mode" ! A.class_ "mode" $ "ERASE (e)"
44 | H.input ! A.id "select-colour"
45 | ! A.class_ "jscolor"
46 | ! A.value "ff7878" -- pastelRed color as defined in Css.Constants
47 | ! A.size "15"
48 | H.table ! A.id "colour-table" $ forM_ (replicate 2 $ replicate 5 "" :: [[H.Html]])
49 | (H.tr . mapM_ (H.td . H.toHtml))
50 | H.div ! A.id "save-graph" ! A.class_ "button" $ "SAVE"
51 | H.input ! A.id "area-of-study"
52 | ! A.class_ "course-code"
53 | ! A.name "course-code"
54 | ! A.placeholder "Enter area of study."
55 | ! A.autocomplete "off"
56 | ! A.type_ "text"
57 | ! A.size "30"
58 | H.div ! A.id "submit-graph-name" ! A.class_ "button" $ "Search for department"
59 | H.div ! A.id "json-data" ! A.class_ "json-data" $ ""
60 |
--------------------------------------------------------------------------------
/app/Response/Image.hs:
--------------------------------------------------------------------------------
1 | module Response.Image
2 | (timetableImageResponse, returnImageData) where
3 |
4 | import Control.Monad.IO.Class (liftIO)
5 | import qualified Data.ByteString as BS
6 | import qualified Data.ByteString.Base64 as BEnc
7 | import qualified Data.Text as T
8 | import Export.GetImages (getTimetableImage)
9 | import Happstack.Server
10 | import System.Directory (removeFile)
11 |
12 | -- | Returns an image of the timetable requested by the user.
13 | timetableImageResponse :: T.Text -> T.Text -> ServerPart Response
14 | timetableImageResponse courses session = do
15 | (svgFilename, imageFilename) <- liftIO $ getTimetableImage courses session
16 | liftIO $ returnImageData svgFilename imageFilename
17 |
18 | -- | Creates and converts an SVG file to an image file, deletes them both and
19 | -- returns the image data as a response.
20 | returnImageData :: String -> String -> IO Response
21 | returnImageData svgFilename imageFilename = do
22 | imageData <- BS.readFile imageFilename
23 | _ <- removeFile imageFilename
24 | _ <- removeFile svgFilename
25 | let encodedData = BEnc.encode imageData
26 | return $ toResponse encodedData
27 |
--------------------------------------------------------------------------------
/app/Response/Loading.hs:
--------------------------------------------------------------------------------
1 | module Response.Loading
2 | (loadingResponse) where
3 |
4 | import Happstack.Server
5 | import MasterTemplate
6 | import Text.Blaze ((!))
7 | import qualified Text.Blaze.Html5 as H
8 | import qualified Text.Blaze.Html5.Attributes as A
9 |
10 | loadingResponse :: ServerPart Response
11 | loadingResponse = do
12 | size <- lookText' "size"
13 | ok $ toResponse $
14 | masterTemplate "Courseography - Loading..."
15 | []
16 | (do
17 | header "Loading..."
18 | if size == "small"
19 | then smallLoadingIcon
20 | else largeLoadingIcon
21 | )
22 | ""
23 |
24 | {- Insert a large loading icon into the page -}
25 | largeLoadingIcon :: H.Html
26 | largeLoadingIcon = H.div ! A.id "loading-icon" $ do
27 | H.img ! A.id "c-logo" ! A.src "/static/res/img/C-logo.png"
28 | H.img ! A.id "compass" ! A.class_ "spinner" ! A.src "/static/res/img/compass.png"
29 |
30 | {- Insert a small loading icon into the page -}
31 | smallLoadingIcon :: H.Html
32 | smallLoadingIcon = H.div ! A.id "loading-icon" $ do
33 | H.img ! A.id "c-logo-small" ! A.src "/static/res/img/C-logo-small.png"
34 | H.img ! A.id "compass-small" ! A.class_ "spinner" ! A.src "/static/res/img/compass-small.png"
35 |
--------------------------------------------------------------------------------
/app/Response/NotFound.hs:
--------------------------------------------------------------------------------
1 | module Response.NotFound
2 | (notFoundResponse) where
3 |
4 | import Happstack.Server
5 | import Text.Blaze ((!))
6 | import qualified Text.Blaze.Html5 as H
7 | import qualified Text.Blaze.Html5.Attributes as A
8 | import Util.Blaze
9 |
10 | notFoundResponse :: ServerPart Response
11 | notFoundResponse =
12 | notFound $ toResponse $
13 | H.html $ do
14 | H.head $ do
15 | H.title "Courseography - 404!"
16 | H.meta ! A.httpEquiv "Content-Type"
17 | ! A.content "text/html;charset=utf-8"
18 | toStylesheet "/static/style/app.css"
19 |
20 | H.body notFoundContent
21 |
22 | notFoundContent :: H.Html
23 | notFoundContent =
24 | H.div ! A.id "contentDiv" $ do
25 | H.h2 "404 Page Not Found!"
26 | H.p "Sorry, the path you have traversed has no destination node."
27 | H.p "The page might have been moved or deleted, or the little dragon running our server might have gone to have smores."
28 | H.p "You can use the links below to get back on the grid or graph."
29 | H.ul ! A.id "links" $ do
30 | H.li $ toLink "/graph" "Graph"
31 | H.li $ toLink "/grid" "Grid"
32 |
--------------------------------------------------------------------------------
/app/Response/README.md:
--------------------------------------------------------------------------------
1 | Responses
2 | =========
3 |
4 | This directory contains all of the Haskell files responsible for creating server responses.
5 | These modules should all be exported by `hs/Response.hs`.
6 |
7 | All of the responses contain a value `???Response` of type `ServerPart Response`,
8 | or a function that returns such a value.
9 | These are the values which are the actual responses sent to the client.
10 | Many use the [blaze-html](https://hackage.haskell.org/package/blaze-html)
11 | library to generate an HTML response. Others use plain [happstack functions](https://hackage.haskell.org/package/happstack-server-7.4.4/docs/Happstack-Server-Response.html)
12 | to serve responses of different types.
13 |
--------------------------------------------------------------------------------
/app/Routes.hs:
--------------------------------------------------------------------------------
1 | module Routes
2 | (routeResponses) where
3 |
4 | import Control.Monad (MonadPlus (mplus), msum)
5 | import Controllers.Course as CoursesController (courseInfo, index, retrieveCourse)
6 | import Controllers.Generate as GenerateController (findAndSavePrereqsResponse, generateResponse)
7 | import Controllers.Graph as GraphsController (getGraphJSON, graphImageResponse, graphResponse,
8 | index)
9 | import Controllers.Timetable as TimetableController
10 | import Database.CourseInsertion (saveGraphJSON)
11 | import Happstack.Server (Browsing (DisableBrowsing), Response, ServerPart, ServerPartT,
12 | ToMessage (toResponse), dir, noTrailingSlash, nullDir, seeOther,
13 | serveDirectory)
14 | import Response (aboutResponse, drawResponse, loadingResponse, notFoundResponse)
15 |
16 | routeResponses :: String -> ServerPartT IO Response
17 | routeResponses staticDir =
18 | msum (map strictMatchDir strictRoutes ++
19 | [dir "static" $ serveDirectory DisableBrowsing [] staticDir,
20 | nullDir >> seeOther ("graph" :: String) (toResponse ("Redirecting to /graph" :: String)),
21 | notFoundResponse])
22 |
23 | strictRoutes :: [(String, ServerPart Response)]
24 | strictRoutes = [
25 | ("grid", TimetableController.gridResponse),
26 | ("graph", GraphsController.graphResponse),
27 | ("graph-generate", GenerateController.findAndSavePrereqsResponse),
28 | ("image", graphImageResponse),
29 | ("timetable-image", TimetableController.exportTimetableImageResponse),
30 | ("timetable-pdf", TimetableController.exportTimetablePDFResponse),
31 | ("draw", drawResponse),
32 | ("about", aboutResponse),
33 | ("graphs", GraphsController.index),
34 | ("generate", generateResponse),
35 | ("get-json-data", getGraphJSON),
36 | ("course", CoursesController.retrieveCourse),
37 | ("courses", CoursesController.index),
38 | ("course-info", CoursesController.courseInfo),
39 | ("calendar", TimetableController.calendarResponse),
40 | ("loading", loadingResponse),
41 | ("save-json", saveGraphJSON)
42 | ]
43 |
44 | strictMatchDir :: (String, ServerPart Response) -> ServerPartT IO Response
45 | strictMatchDir (pathname, response) =
46 | mplus (do noTrailingSlash -- enforce no trailing slash in the URI
47 | dir pathname nullDir -- enforce that no segments occur after pathname
48 | response)
49 | (do dir pathname nullDir -- if a trailing slash exists, redirect
50 | seeOther ("/" ++ pathname) (toResponse ("Redirecting to /" ++ pathname)))
51 |
--------------------------------------------------------------------------------
/app/Scripts.hs:
--------------------------------------------------------------------------------
1 | module Scripts (
2 | graphScripts, timetableScripts, drawScripts, postScripts, searchScripts, generateScripts, aboutScripts,
3 | )
4 | where
5 |
6 | import Text.Blaze ((!))
7 | import qualified Text.Blaze.Html5 as H
8 | import qualified Text.Blaze.Html5.Attributes as A
9 | import Util.Blaze
10 |
11 | graphScripts :: H.Html
12 | graphScripts = H.script ! A.src "/static/js/graph/app.js" $ ""
13 |
14 | timetableScripts :: H.Html
15 | timetableScripts = H.script ! A.src "/static/js/grid/app.js" $ ""
16 |
17 | drawScripts :: H.Html
18 | drawScripts = do
19 | mapM_ toScript
20 | ["/static/js/draw/variables.js",
21 | "/static/js/draw/path.js",
22 | "/static/js/draw/setup.js",
23 | "/static/js/vendor/jscolor.min.js"]
24 | H.script ! A.src "/static/js/draw/app.js" $ ""
25 |
26 | postScripts :: H.Html
27 | postScripts = do
28 | mapM_ toScript []
29 | H.script ! A.src "/static/js/post/app.js" $ ""
30 |
31 | searchScripts :: H.Html
32 | searchScripts = do
33 | H.script ! A.src "/static/js/search/app.js" $ ""
34 |
35 | generateScripts :: H.Html
36 | generateScripts = do
37 | H.script ! A.src "/static/js/generate/app.js" $ ""
38 |
39 | aboutScripts :: H.Html
40 | aboutScripts = do
41 | H.script ! A.src "/static/js/about/app.js" $ ""
42 |
--------------------------------------------------------------------------------
/app/Server.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE CPP, OverloadedStrings #-}
2 |
3 | {-|
4 | Description: Configure and run the server for Courseography.
5 | This module defines the configuration for the server, including logging.
6 | It also defines all of the allowed server routes, and the corresponding
7 | responses.
8 | -}
9 | module Server
10 | (runServer) where
11 |
12 | import Config (logFilePath, serverConf)
13 | import Control.Concurrent (forkIO, killThread)
14 | import Control.Monad (when)
15 | import Data.String (fromString)
16 | import Filesystem.Path.CurrentOS as Path
17 | import Happstack.Server hiding (host)
18 | import Routes (routeResponses)
19 | import System.Directory (getCurrentDirectory)
20 | import System.IO (BufferMode (LineBuffering), hSetBuffering, stderr, stdout)
21 | import System.Log.Handler.Simple (fileHandler)
22 | import System.Log.Logger (Priority (INFO), rootLoggerName, setHandlers, setLevel,
23 | updateGlobalLogger)
24 |
25 | runServer :: IO ()
26 | runServer = do
27 | configureLogger
28 | staticDir <- getStaticDir
29 |
30 | -- Start the HTTP server
31 | server <- serverConf
32 | httpThreadId <- forkIO $ simpleHTTP server $ do
33 | decodeBody (defaultBodyPolicy "/tmp/" 4096 4096 4096)
34 | routeResponses staticDir
35 | waitForTermination
36 | killThread httpThreadId
37 | where
38 | -- | Global logger configuration.
39 | configureLogger :: IO ()
40 | configureLogger = do
41 | -- Use line buffering to ensure logging messages are printed correctly
42 | hSetBuffering stdout LineBuffering
43 | hSetBuffering stderr LineBuffering
44 | -- Set log level to INFO so requests are logged to stdout
45 | updateGlobalLogger rootLoggerName $ setLevel INFO
46 | -- Log to file if a file path is provided
47 | logFile <- logFilePath
48 | when (logFile /= "") $ do
49 | fileH <- fileHandler logFile INFO
50 | updateGlobalLogger rootLoggerName $ setHandlers [fileH]
51 |
52 |
53 | -- | Return the directory where all static files are stored.
54 | -- Note: the type here is System.IO.FilePath, not FileSystem.Path.FilePath.
55 | getStaticDir :: IO Prelude.FilePath
56 | getStaticDir = do
57 | cwd <- getCurrentDirectory
58 | --let parentDir = Path.parent $ Path.decodeString cwd
59 | --return $ Path.encodeString $ Path.append parentDir $ fromString "public/"
60 | return $ Path.encodeString $ Path.append (Path.decodeString cwd) $ fromString "public/"
61 |
--------------------------------------------------------------------------------
/app/Svg/Database.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Description: Helpers for interacting with the graph database. Incomplete.
3 |
4 | This module is responsible for inserting values associated with the graphs
5 | into the database. These functions are just helpers for the main function
6 | in "Svg.Parser", though it is possible to put all of the data retrieval code in
7 | here as well at some point in the future.
8 | -}
9 |
10 | module Svg.Database
11 | (insertGraph, insertElements, deleteGraph) where
12 |
13 | import qualified Data.Text as T
14 | import Database.Persist.Sqlite
15 | import Database.Tables hiding (graphDynamic, graphHeight, graphWidth, paths, shapes, texts)
16 |
17 | -- | Insert a new graph into the database, returning the key of the new graph.
18 | insertGraph :: T.Text -- ^ The title of the graph that is being inserted.
19 | -> Double -- ^ The width dimension of the graph
20 | -> Double -- ^ The height dimension of the graph
21 | -> Bool -- ^ True if graph is dynamically generated
22 | -> SqlPersistM GraphId -- ^ The unique identifier of the inserted graph.
23 | insertGraph graphName graphWidth graphHeight graphDynamic = do
24 | runMigration migrateAll
25 | insert (Graph graphName graphWidth graphHeight graphDynamic)
26 |
27 | -- | Insert graph components into the database.
28 | insertElements :: ([Path], [Shape], [Text]) -> SqlPersistM ()
29 | insertElements (paths, shapes, texts) = do
30 | mapM_ insert_ shapes
31 | mapM_ insert_ paths
32 | mapM_ insert_ texts
33 |
34 | -- | Delete a graph with the given graph ID from the database.
35 | deleteGraph :: Key Graph -> SqlPersistM ()
36 | deleteGraph gId = do
37 | deleteWhere [TextGraph ==. gId]
38 | deleteWhere [ShapeGraph ==. gId]
39 | deleteWhere [PathGraph ==. gId]
40 | deleteWhere [GraphId ==. gId]
41 |
--------------------------------------------------------------------------------
/app/Svg/README.md:
--------------------------------------------------------------------------------
1 | Information
2 | ===========
3 |
4 | Summary
5 | -------
6 |
7 | app/Svg contains modules for interacting with the SVG files, for instance
8 | putting the SVG files on the graph page.
9 |
10 | Notable Files
11 | -------------
12 |
13 | Parser.hs is the main graph module, using the other modules it outputs the
14 | final svg files in @public\/res\/graphs\/gen@.
15 |
--------------------------------------------------------------------------------
/app/Util/Blaze.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : Util.Blaze
3 | Description : Contains methods for setting various HTML attributes.
4 | -}
5 | module Util.Blaze
6 | (toStylesheet,
7 | toScript,
8 | toLink,
9 | mdToHTML) where
10 |
11 | import qualified Data.Text as T
12 | import Data.Text.Lazy (Text)
13 | import Text.Blaze ((!))
14 | import qualified Text.Blaze.Html5 as H
15 | import qualified Text.Blaze.Html5.Attributes as A
16 | import Text.Markdown (def, markdown)
17 |
18 | -- |Sets the html attributes to the href of the style sheet.
19 | toStylesheet :: T.Text -> H.Html
20 | toStylesheet href = H.link ! A.rel "stylesheet"
21 | ! A.type_ "text/css"
22 | ! A.href (H.textValue href)
23 |
24 | -- |Sets the script attributes.
25 | toScript :: T.Text -> H.Html
26 | toScript src = H.script ! A.src (H.textValue src) $ ""
27 |
28 | -- |Creates a link by setting the href attribute.
29 | toLink :: T.Text -> T.Text -> H.Html
30 | toLink link content = H.a ! A.href (H.textValue link)
31 | $ H.toHtml content
32 |
33 | -- | mdToHTML takes in the contents of a file written in Mark Down and converts it to
34 | -- blaze-HTML.
35 | mdToHTML :: Text -> H.Html
36 | mdToHTML = markdown def
37 |
--------------------------------------------------------------------------------
/app/Util/Documentation.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : Util.Documentation
3 | Description : Defines function to generate local Haddock documentation.
4 |
5 | This module contains a shell command to run Haddock to generate documentation
6 | for the Haskell source files of Courseography.
7 | -}
8 | module Util.Documentation where
9 |
10 | import System.Directory (createDirectoryIfMissing)
11 | import System.Process (callCommand)
12 |
13 | -- | Path to documentation directory
14 | docPath :: String
15 | docPath = "doc"
16 |
17 | -- | Generate documentation for Courseography.
18 | generateDocs :: IO ()
19 | generateDocs = do
20 | putStrLn "Generating documentation..."
21 | createDirectoryIfMissing True docPath
22 | callCommand $ unwords [
23 | "stack exec haddock --",
24 | "-o",
25 | docPath,
26 | "-h",
27 | "--optghc=-iapp",
28 | "--optghc=-XOverloadedStrings",
29 | "--optghc=-XPartialTypeSignatures",
30 | "--optghc=-XScopedTypeVariables",
31 | "--ignore-all-exports",
32 | "app/Main.hs"
33 | ]
34 |
--------------------------------------------------------------------------------
/app/Util/Happstack.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : Util.Happstack
3 | Description : Contains a single method for creating a JSON response.
4 | -}
5 | module Util.Happstack
6 | (createJSONResponse) where
7 |
8 | import Data.Aeson (ToJSON, encode)
9 | import qualified Data.ByteString.Char8 as BS
10 | import Happstack.Server
11 |
12 | -- | Creates a JSON response.
13 | createJSONResponse :: ToJSON a => a -> Response
14 | createJSONResponse x = toResponseBS (BS.pack "application/json") (encode x)
15 |
--------------------------------------------------------------------------------
/app/Util/README.md:
--------------------------------------------------------------------------------
1 | Utility Modules
2 | ===============
3 |
4 | This directory contains modules which define utility functions for Courseography.
5 | These functions should not be specific to Courseography, but instead
6 | general-purpose and useful in other contexts.
7 | Some files are simply convenient helpers built on other libraries (like `Util.Blaze`),
8 | and are named accordingly.
9 |
--------------------------------------------------------------------------------
/app/WebParsing/Ligature.hs:
--------------------------------------------------------------------------------
1 | {- | Utilities for expanding ligatures in unicode text.
2 |
3 | Currently this module expands the following ligatures:ff,fi,fl,ffi,ffl,ſt,st
4 |
5 | From https://hackage.haskell.org/package/ligature.
6 | -}
7 | module WebParsing.Ligature where
8 |
9 | import Prelude hiding (concatMap)
10 |
11 | import Data.Text
12 |
13 | -- | If a character is a ligature, expand it to several characters
14 | expandLigature :: Char -> Text
15 | expandLigature '\xFB00' = cons 'f' (cons 'f' empty)
16 | expandLigature '\xFB01' = cons 'f' (cons 'i' empty)
17 | expandLigature '\xFB02' = cons 'f' (cons 'l' empty)
18 | expandLigature '\xFB03' = cons 'f' (cons 'f' (cons 'i' empty))
19 | expandLigature '\xFB04' = cons 'f' (cons 'f' (cons 'l' empty))
20 | expandLigature '\xFB05' = cons 'f' (cons 't' empty)
21 | expandLigature '\xFB06' = cons 's' (cons 't' empty)
22 | expandLigature c = singleton c
23 |
24 | -- | Expand all ligatures in the text
25 | expand :: Text -> Text
26 | expand = concatMap expandLigature
27 |
--------------------------------------------------------------------------------
/app/WebParsing/ParsecCombinators.hs:
--------------------------------------------------------------------------------
1 | module WebParsing.ParsecCombinators
2 | (getCourseFromTag,
3 | findCourseFromTag,
4 | text,
5 | parseUntil) where
6 |
7 | import qualified Data.Text as T
8 | import qualified Text.Parsec as P
9 | import Text.Parsec.Text (Parser)
10 |
11 |
12 | getCourseFromTag :: T.Text -> T.Text
13 | getCourseFromTag courseTag =
14 | let course = P.parse findCourseFromTag "(source)" courseTag
15 | in
16 | case course of
17 | Right courseName -> courseName
18 | Left _ -> ""
19 |
20 | findCourseFromTag :: Parser T.Text
21 | findCourseFromTag = do
22 | _ <- P.string "/course/"
23 | parsed <- P.many1 P.anyChar
24 | return $ T.pack parsed
25 |
26 | parseUntil :: Parser a -> Parser T.Text
27 | parseUntil parser = do
28 | parsed <- P.manyTill P.anyChar (P.try parser)
29 | return $ T.pack parsed
30 |
31 | text :: T.Text -> Parser T.Text
32 | text someText = do
33 | parsed <- mapM P.char (T.unpack someText)
34 | return $ T.pack parsed
35 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "useBuiltIns": "usage",
7 | "corejs": { "version": "3.30", "proposals": true }
8 | }
9 | ],
10 | "@babel/preset-react"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/backend-test/Controllers/ControllerTests.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Description: Contoroller test suites module.
3 |
4 | Module that contains the test suites for all the controllers.
5 |
6 | -}
7 |
8 | module Controllers.ControllerTests
9 | ( controllerTests ) where
10 |
11 | import Test.HUnit (Test (..))
12 |
13 | import Controllers.CourseControllerTests (courseControllerTestSuite)
14 | import Controllers.GraphControllerTests (graphControllerTestSuite)
15 |
16 | -- Single test encompassing all controller test suites
17 | controllerTests :: Test
18 | controllerTests = TestList [courseControllerTestSuite, graphControllerTestSuite]
19 |
--------------------------------------------------------------------------------
/backend-test/Controllers/GraphControllerTests.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Description: Graph Controller module tests.
3 |
4 | Module that contains the tests for the functions in the Graph Controller module.
5 |
6 | -}
7 |
8 | module Controllers.GraphControllerTests
9 | ( graphControllerTestSuite
10 | ) where
11 |
12 | import Config (runDb)
13 | import Controllers.Graph (index)
14 | import qualified Data.ByteString.Lazy.Char8 as BL
15 | import qualified Data.Text as T
16 | import Database.Persist.Sqlite (SqlPersistM, insert_)
17 | import Database.Tables (Graph (..))
18 | import Happstack.Server (rsBody)
19 | import Test.HUnit (Test (..), assertEqual)
20 | import TestHelpers (clearDatabase, runServerPart)
21 |
22 | -- | List of test cases as (label, input graphs, expected output)
23 | indexTestCases :: [(String, [T.Text], String)]
24 | indexTestCases =
25 | [ ("No graphs",
26 | [],
27 | "[]"
28 | ),
29 |
30 | ("One graph",
31 | ["Statistics"],
32 | "[{\"dynamic\":false,\"height\":0,\"id\":1,\"title\":\"Statistics\",\"width\":0}]"
33 | ),
34 |
35 | ("Multiple graphs",
36 | ["Computer Science", "Statistics", "Physics"],
37 | "[{\"dynamic\":false,\"height\":0,\"id\":1,\"title\":\"Computer Science\",\"width\":0},{\"dynamic\":false,\"height\":0,\"id\":3,\"title\":\"Physics\",\"width\":0},{\"dynamic\":false,\"height\":0,\"id\":2,\"title\":\"Statistics\",\"width\":0}]" )
38 | ]
39 |
40 | -- | Helper function to insert graphs into the database
41 | insertGraphs :: [T.Text] -> SqlPersistM ()
42 | insertGraphs = mapM_ insertGraph
43 | where
44 | insertGraph title = insert_ (Graph title 0 0 False )
45 |
46 | -- | Run a test case (case, input, expected output) on the index function.
47 | runIndexTest :: String -> [T.Text] -> String -> Test
48 | runIndexTest label graphs expected =
49 | TestLabel label $ TestCase $ do
50 | runDb $ do
51 | clearDatabase
52 | insertGraphs graphs
53 | response <- runServerPart Controllers.Graph.index
54 | let actual = BL.unpack $ rsBody response
55 | assertEqual ("Unexpected response body for " ++ label) expected actual
56 |
57 | -- | Run all the index test cases
58 | runIndexTests :: [Test]
59 | runIndexTests = map (\(label, graphs, expected) -> runIndexTest label graphs expected) indexTestCases
60 |
61 | -- | Test suite for Graph Controller Module
62 | graphControllerTestSuite :: Test
63 | graphControllerTestSuite = TestLabel "Graph Controller tests" $ TestList runIndexTests
64 |
--------------------------------------------------------------------------------
/backend-test/Database/CourseQueriesTests.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Description: CourseQueries module tests.
3 |
4 | Module that contains the tests for the functions in the CourseQueries module.
5 |
6 | -}
7 |
8 | module Database.CourseQueriesTests
9 | ( courseQueriesTestSuite
10 | ) where
11 |
12 | import Config (runDb)
13 | import Control.Monad.IO.Class (liftIO)
14 | import qualified Data.Text as T
15 | import Data.Time (getCurrentTime)
16 | import Database.CourseQueries (reqsForPost)
17 | import Database.DataType (PostType(..))
18 | import Database.Persist.Sqlite (insert_)
19 | import Database.Tables (Post(..))
20 | import Test.HUnit (Test(..), assertEqual)
21 | import TestHelpers (clearDatabase)
22 |
23 | -- | List of test cases as (label, requirements to insert, input program, expected output)
24 | reqsForPostTestCases :: [(String, T.Text, T.Text, String)]
25 | reqsForPostTestCases =
26 | [ ("No program", "", "", "[]")
27 | , ("Valid program", "/CSC199H1/", "ASMAJ1689", "[\"CSC199H1\"]")
28 | , ("Invalid program", "", "ABCDE1234", "[]")
29 | ]
30 |
31 | -- | Run a test case (case, requirements, input, expected output) on the reqsForPost function.
32 | runReqsForPostTest :: String -> T.Text -> T.Text -> String -> Test
33 | runReqsForPostTest label reqsToInsert program expected =
34 | TestLabel label $ TestCase $ do
35 | currentTime <- liftIO getCurrentTime
36 | let testPost = Post Major "Computer Science" program "Sample post description" reqsToInsert currentTime currentTime
37 |
38 | runDb $ do
39 | clearDatabase
40 | insert_ testPost
41 |
42 | let requirements = reqsForPost testPost
43 | let actual = show requirements
44 | assertEqual ("Unexpected response body for " ++ label) expected actual
45 |
46 | -- | Run all the reqsForPost test cases
47 | runReqsForPostTests :: [Test]
48 | runReqsForPostTests = map (\(label, reqsToInsert, program, expected) -> runReqsForPostTest label reqsToInsert program expected) reqsForPostTestCases
49 |
50 | -- | Test suite for CourseQueries Module
51 | courseQueriesTestSuite :: Test
52 | courseQueriesTestSuite = TestLabel "Course Queries tests" $ TestList runReqsForPostTests
53 |
--------------------------------------------------------------------------------
/backend-test/Database/DatabaseTests.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Description: Database test suites module.
3 |
4 | Module that contains the test suites for all the database functions.
5 |
6 | -}
7 |
8 | module Database.DatabaseTests
9 | ( databaseTests ) where
10 |
11 | import Test.HUnit (Test (..))
12 | import Database.CourseQueriesTests (courseQueriesTestSuite)
13 |
14 | -- Single test encompassing all database test suites
15 | databaseTests :: Test
16 | databaseTests = TestList [courseQueriesTestSuite]
17 |
--------------------------------------------------------------------------------
/backend-test/Main.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Description: Main module for testing.
3 |
4 | Module that acts as interface for testing multiple test suites using cabal.
5 |
6 | -}
7 |
8 | module Main where
9 |
10 | import Control.Monad (when)
11 | import Config (databasePath)
12 | import Data.Text (unpack)
13 | import Database.Database(setupDatabase)
14 | import System.Directory (removeFile)
15 | import System.Environment (setEnv, unsetEnv)
16 | import qualified System.Exit as Exit
17 | import Test.HUnit (Test (..), failures, runTestTT)
18 | import RequirementTests.RequirementTests (requirementTests)
19 | import Controllers.ControllerTests (controllerTests)
20 | import Database.DatabaseTests (databaseTests)
21 | import SvgTests.SvgTests (svgTests)
22 |
23 | tests :: IO Test
24 | tests = do
25 | return $ TestList [requirementTests, controllerTests, svgTests, databaseTests]
26 |
27 | main :: IO ()
28 | main = do
29 | setEnv "APP_ENV" "test"
30 | setupDatabase
31 | testSuites <- tests
32 | count <- runTestTT testSuites
33 | when (failures count > 0) Exit.exitFailure
34 | path <- databasePath
35 | removeFile $ unpack path
36 | unsetEnv "APP_ENV"
37 |
--------------------------------------------------------------------------------
/backend-test/RequirementTests/ModifierTests.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Description: Test modifier string formatting using HUnit Testing Framework.
3 |
4 | Module containing test cases for Modifier string formatters.
5 |
6 | -}
7 |
8 | module RequirementTests.ModifierTests
9 | ( modifierTestSuite ) where
10 |
11 | import Database.Requirement
12 | import DynamicGraphs.GraphNodeUtils (concatModOr, stringifyModAnd)
13 | import Test.HUnit (Test (..), assertEqual)
14 |
15 | -- Function to facilitate test case creation given a string, Req tuple
16 | createTest :: (Eq a, Show a) => (a -> String) -> String -> [(a, String)] -> Test
17 | createTest function label input = TestLabel label $ TestList $ map (\(x, y) ->
18 | TestCase $ assertEqual ("for (" ++ y ++ ")")
19 | y (function x)) input
20 |
21 | -- Global FCEs value so the expected output has the same FCEs as the partial function in createTest
22 | globalFces :: Float
23 | globalFces = 1.0
24 |
25 | concatModOrInputs :: [([Modifier], String)]
26 | concatModOrInputs = [
27 | ([Department "CSC", Department "BCB"], "CSC/BCB")
28 | , ([Department "CSC", Department "BCB", Department "Statistics"], "CSC/BCB/Statistics")
29 | , ([Level "300", Level "400"], "300/400")
30 | , ([Requirement (Raw "lorem"), Requirement (Raw "ipsum")], "lorem or ipsum")
31 | ]
32 |
33 | simpleModAndInputs :: [([Modifier], String)]
34 | simpleModAndInputs = [
35 | ([Department "CSC", Level "300"], show globalFces ++ " CSC FCEs at the 300 level")
36 | , ([Department "CSC", Requirement (Raw "some raw text")], show globalFces ++ " CSC FCEs from some raw text")
37 | , ([Level "300+", Requirement (Raw "some raw text")], show globalFces ++ " FCEs at the 300+ level from some raw text")
38 | , ([Department "CSC", Level "300+", Requirement (Raw "some raw text")], show globalFces ++ " CSC FCEs at the 300+ level from some raw text")
39 | , ([ModOr [Level "300", Level "400"], Department "CSC"], show globalFces ++ " CSC FCEs at the 300/400 level")
40 | ]
41 |
42 | modandModOrInputs :: [([Modifier], String)]
43 | modandModOrInputs = [
44 | ([ModOr [Level "300", Level "400"], Department "CSC"], show globalFces ++ " CSC FCEs at the 300/400 level")
45 | , ([Level "300+", ModOr [Department "CSC", Department "BCB", Department "Statistics"]], show globalFces ++ " CSC/BCB/Statistics FCEs at the 300+ level")
46 | , ([ModOr [Level "300", Level "400"], ModOr [Department "CSC", Department "BCB"]], show globalFces ++ " CSC/BCB FCEs at the 300/400 level")
47 | , ([ModOr [Level "300", Level "400"], ModOr [Department "CSC", Department "BCB"], Requirement (Raw "some raw text")], show globalFces ++ " CSC/BCB FCEs at the 300/400 level from some raw text")
48 | ]
49 |
50 | concatModOrTests :: Test
51 | concatModOrTests = createTest concatModOr "joining ModOr with a delimiter" concatModOrInputs
52 |
53 | simpleModAndTests :: Test
54 | simpleModAndTests = createTest (stringifyModAnd globalFces) "ModAnd not containing ModOrs" simpleModAndInputs
55 |
56 | modandModOrTests :: Test
57 | modandModOrTests = createTest (stringifyModAnd globalFces) "ModAnd containing ModOrs" modandModOrInputs
58 |
59 | -- functions for running tests in REPL
60 | modifierTestSuite :: Test
61 | modifierTestSuite = TestLabel "ReqParser tests" $ TestList [concatModOrTests, simpleModAndTests, modandModOrTests]
62 |
--------------------------------------------------------------------------------
/backend-test/RequirementTests/PostParserTests.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Description: Test Post Parsers using HUnit Testing Framework.
3 |
4 | Module containing test cases for Post Parsers.
5 |
6 | -}
7 |
8 | module RequirementTests.PostParserTests
9 | ( postTestSuite ) where
10 |
11 | import Data.Bifunctor (second)
12 | import qualified Data.Text as T
13 | import Database.DataType (PostType (..))
14 | import Test.HUnit (Test (..), assertEqual)
15 | import qualified Text.Parsec as Parsec
16 | import WebParsing.PostParser (getPostType, postInfoParser)
17 |
18 | -- Function to facilitate test case creation given a string, Req tuple
19 | createTest :: (Show a, Eq a, Show b, Eq b) => (a -> b) -> String -> [(a, b)] -> Test
20 | createTest function label input = TestLabel label $ TestList $ map (\(x, y) ->
21 | TestCase $ assertEqual ("for (" ++ show x ++ "),")
22 | y (function x)) input
23 |
24 | -- | Input and output pair of each post
25 | -- | Output is in the order of (postDepartment, postCode, postName)
26 | postInfoInputs :: [(T.Text, (T.Text, T.Text))]
27 | postInfoInputs = [
28 | ("Music Major (Arts Program) - ASMAJ2276",
29 | ("Music Major (Arts Program)", "ASMAJ2276"))
30 | , ("Focus in Artificial Intelligence (Major) - ASFOC1689K",
31 | ("Focus in Artificial Intelligence (Major)", "ASFOC1689K"))
32 | , ("Cell & Molecular Biology Specialist: Focus in Molecular Networks of the Cell - ASSPE1003A",
33 | ("Cell & Molecular Biology Specialist: Focus in Molecular Networks of the Cell", "ASSPE1003A"))
34 | , ("Focus in Medical Anthropology (Specialist: Society, Culture and Language) - ASFOC2112B",
35 | ("Focus in Medical Anthropology (Specialist: Society, Culture and Language)", "ASFOC2112B"))
36 | , ("Anthropology Specialist (Society, Culture, and Language) (Arts Program) - ASSPE2112",
37 | ("Anthropology Specialist (Society, Culture, and Language) (Arts Program)", "ASSPE2112"))
38 | , ("Christianity and Culture: Major Program in Religious Education (Arts Program) - ASMAJ1021",
39 | ("Christianity and Culture: Major Program in Religious Education (Arts Program)", "ASMAJ1021"))
40 | , ("Minor in French Language (Arts Program) - ASMIN0120",
41 | ("Minor in French Language (Arts Program)", "ASMIN0120"))
42 | , ("Minor Program in Christianity and Education (Arts Program) - ASMIN1014",
43 | ("Minor Program in Christianity and Education (Arts Program)", "ASMIN1014"))
44 | , ("Psychology Research Specialist - Thesis (Science Program) - ASSPE1958)",
45 | ("Psychology Research Specialist - Thesis (Science Program)", "ASSPE1958"))
46 | , ("Cognitive Science Major - Arts (Language and Cognition Stream) (Arts Program) - ASMAJ1445B",
47 | ("Cognitive Science Major - Arts (Language and Cognition Stream) (Arts Program)", "ASMAJ1445B"))
48 | , ("Certificate in Business Fundamentals - ASCER2400",
49 | ("Certificate in Business Fundamentals", "ASCER2400"))
50 | , ("Focus in Finance - ASFOC2431B",
51 | ("Focus in Finance", "ASFOC2431B"))
52 | , ("Focus in Green Chemistry",
53 | ("Focus in Green Chemistry", ""))
54 | , ("Biological Physics Specialist",
55 | ("Biological Physics Specialist", ""))
56 | ]
57 |
58 | getPostTypeInputs :: [((T.Text, T.Text), PostType)]
59 | getPostTypeInputs = [
60 | (("ASSPE1958", "Psychology Specialist"), Specialist)
61 | , (("ASMAJ2276", "Music Major"), Major)
62 | , (("ASMIN0120", "Minor in French"), Minor)
63 | , (("ASFOC1689B", "Focus in AI"), Focus)
64 | , (("ASCER2400", "Certificate in Business"), Certificate)
65 | , (("", "Psychology Specialist"), Specialist)
66 | , (("", "Music Major"), Major)
67 | , (("", "Minor in French"), Minor)
68 | , (("", "Focus in AI"), Focus)
69 | , (("", "Certificate in Business"), Certificate)
70 | ]
71 |
72 | postInfoTests :: Test
73 | postInfoTests = createTest (Parsec.parse postInfoParser "") "Post requirements" $ map (second Right) postInfoInputs
74 |
75 | getPostTypeTests :: Test
76 | getPostTypeTests = createTest (uncurry getPostType) "Post requirements" getPostTypeInputs
77 |
78 | -- functions for running tests in REPL
79 | postTestSuite :: Test
80 | postTestSuite = TestLabel "PostParser tests" $ TestList [postInfoTests, getPostTypeTests]
81 |
--------------------------------------------------------------------------------
/backend-test/RequirementTests/PreProcessingTests.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Description: Test some pre-processing functions that clean up the text before running the parsers
3 |
4 | -}
5 |
6 | module RequirementTests.PreProcessingTests
7 | ( preProcTestSuite ) where
8 |
9 | import Data.Text as T hiding (map)
10 | import Test.HUnit (Test (..), assertEqual)
11 | import Text.HTML.TagSoup (Tag (..))
12 | import WebParsing.PostParser (pruneHtml)
13 |
14 | createTest :: (Eq a, Show a, Eq b, Show b) => (a -> b) -> String -> [(a, b)] -> Test
15 | createTest function label input = TestLabel label $ TestList $ map (\(x, y) ->
16 | TestCase $ assertEqual ("for (" ++ show y ++ ")")
17 | y (function x)) input
18 |
19 |
20 | pruneHtmlInputs :: [([Tag T.Text], [Tag T.Text])]
21 | pruneHtmlInputs = [
22 | ( [TagOpen "h1" [("class", "c1 c2")], TagText "reqs", TagClose "h1"]
23 | , [TagOpen "h1" [], TagText"reqs", TagClose "h1"]
24 | ),
25 | ( [TagOpen "h2" [("style", "some: styles")], TagText "reqs", TagClose "h2"]
26 | , [TagOpen "h2" [("style", "some: styles")], TagText "reqs", TagClose "h2"]
27 | ),
28 | ( [TagOpen "h3" [("class", "c1 c2"), ("style", "some: styles")], TagText "reqs", TagClose "h3"]
29 | , [TagOpen "h3" [("style", "some: styles")], TagText "reqs", TagClose "h3"]
30 | ),
31 | ( [TagOpen "h4" [], TagOpen "a" [("href", "/CSC401H1")], TagText "CSC401H1", TagClose "a", TagClose "h4"]
32 | , [TagOpen "h4" [], TagText "CSC401H1", TagClose "h4"]
33 | )
34 | ]
35 |
36 | pruneHtmlTests :: Test
37 | pruneHtmlTests = createTest pruneHtml "filtering out html attributes" pruneHtmlInputs
38 |
39 | -- functions for running tests in REPL
40 | preProcTestSuite :: Test
41 | preProcTestSuite = TestLabel "Pre-processing tests" $ TestList [pruneHtmlTests]
42 |
--------------------------------------------------------------------------------
/backend-test/RequirementTests/RequirementTests.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Description: Requirement test suites module.
3 |
4 | Module that contains the test suites for all the requirement tests.
5 |
6 | -}
7 |
8 | module RequirementTests.RequirementTests
9 | ( requirementTests ) where
10 |
11 | import Test.HUnit (Test (..))
12 | import RequirementTests.ModifierTests (modifierTestSuite)
13 | import RequirementTests.PostParserTests (postTestSuite)
14 | import RequirementTests.PreProcessingTests (preProcTestSuite)
15 | import RequirementTests.ReqParserTests (reqTestSuite)
16 |
17 | -- Single test encompassing all requirement test suites
18 | requirementTests :: Test
19 | requirementTests = TestList [reqTestSuite, postTestSuite, preProcTestSuite, modifierTestSuite]
20 |
--------------------------------------------------------------------------------
/backend-test/SvgTests/SvgTests.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Description: SVG test suites module.
3 |
4 | Module that contains the test suites for all the SVG tests.
5 |
6 | -}
7 |
8 | module SvgTests.SvgTests (svgTests) where
9 |
10 | import Test.HUnit (Test (..))
11 | import SvgTests.IntersectionTests (intersectionTestSuite)
12 |
13 | -- Single test encompassing all svg test suites
14 | svgTests :: Test
15 | svgTests = TestList [intersectionTestSuite]
16 |
--------------------------------------------------------------------------------
/config.yaml:
--------------------------------------------------------------------------------
1 | # Server Configuration Settings
2 | port: 8000
3 | logMessage: "Happstack.Server.AccessLog.Combined"
4 | logFile: "" # The file path to log server access to, e.g. "path/to/courseography.log"
5 |
6 | # Database Path
7 | databasePath: "db/database.sqlite3"
8 |
9 | # File Path Strings
10 | graphPath: "./graphs/"
11 | genCssPath: "./public/style/"
12 |
13 | # URLs
14 | timetableUrl: "https://ttb.utoronto.ca/"
15 | timetableApiUrl: "https://api.easi.utoronto.ca/ttb/getPageableCourses"
16 | fasCalendarUrl: "https://artsci.calendar.utoronto.ca/"
17 | programsUrl: "https://artsci.calendar.utoronto.ca/listing-program-subject-areas"
18 |
19 | # Term dates
20 | fallStartDate: 2024-09-03
21 | fallEndDate: 2024-12-03
22 | winterStartDate: 2025-01-06
23 | winterEndDate: 2025-04-04
24 | outDay: 2026-01-01
25 |
26 | # Holidays
27 | holidaysList:
28 | - 20241014T
29 | - 20241028T
30 | - 20241029T
31 | - 20241030T
32 | - 20241031T
33 | - 20241101T
34 | - 20250217T
35 | - 20250218T
36 | - 20240219T
37 | - 20250220T
38 | - 20250221T
39 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:8000",
3 | "ignoreTestFiles": ["/**/examples/*.js"]
4 | }
5 |
--------------------------------------------------------------------------------
/cypress/integration/graph_spec/bool_spec.js:
--------------------------------------------------------------------------------
1 | describe("Boolean tests", () => {
2 | beforeEach(() => {
3 | cy.visit("/graph")
4 | })
5 |
6 | it("should initially be 'bool' and 'inactive'", () => {
7 | cy.get('[data-testid="and(csc209,csc258)"]')
8 | .should("have.class", "bool")
9 | .should("have.class", "inactive")
10 | })
11 | it("shouldn't do anything when you click or hover over it", () => {
12 | cy.get('[data-testid="and(csc209,csc258)"]')
13 | .should("have.class", "bool")
14 | .should("have.class", "inactive")
15 | cy.get('[data-testid="and(csc209,csc258)"]').click()
16 | cy.get('[data-testid="and(csc209,csc258)"]')
17 | .should("have.class", "bool")
18 | .should("have.class", "inactive")
19 | cy.get('[data-testid="and(csc209,csc258)"]').trigger("mouseover")
20 | cy.get('[data-testid="and(csc209,csc258)"]')
21 | .should("have.class", "bool")
22 | .should("have.class", "inactive")
23 | })
24 |
25 | it("AND should be 'active' when prereq parents are met", () => {
26 | cy.get('[data-testid="and(csc209,csc258)"]').contains("and")
27 | cy.get('[data-testid="csc258"]').click()
28 | cy.get('[data-testid="csc209"]').click()
29 | cy.get('[data-testid="and(csc209,csc258)"]').should("have.class", "active")
30 | cy.get('[data-testid="csc367"]').should("have.class", "takeable")
31 | })
32 | it("OR should be 'active' when 1+ of the prereq parents are met", () => {
33 | cy.get('[data-testid="and(csc463,csc373)"]').contains("or")
34 | cy.get('[data-testid="csc373"]').click()
35 | cy.get('[data-testid="csc463"]').click()
36 | cy.get('[data-testid="and(csc463,csc373)"]').should("have.class", "active")
37 | cy.get('[data-testid="csc438"]').should("have.class", "takeable")
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/cypress/integration/graph_spec/graph_spec.js:
--------------------------------------------------------------------------------
1 | describe("Graph", () => {
2 | beforeEach(() => {
3 | cy.visit("/graph")
4 | })
5 |
6 | describe("Boolean tests", () => {
7 | it("should initially be 'bool' and 'inactive'", () => {
8 | cy.get(`[data-testid="and(csc209,csc258)"]`)
9 | .should("have.class", "bool")
10 | .should("have.class", "inactive")
11 | })
12 | it("shouldn't do anything when you click or hover over it", () => {
13 | cy.get(`[data-testid="and(csc209,csc258)"]`)
14 | .should("have.class", "bool")
15 | .should("have.class", "inactive")
16 | cy.get(`[data-testid="and(csc209,csc258)"]`).click()
17 | cy.get(`[data-testid="and(csc209,csc258)"]`)
18 | .should("have.class", "bool")
19 | .should("have.class", "inactive")
20 | cy.get(`[data-testid="and(csc209,csc258)"]`).trigger("mouseover")
21 | cy.get(`[data-testid="and(csc209,csc258)"]`)
22 | .should("have.class", "bool")
23 | .should("have.class", "inactive")
24 | })
25 |
26 | it("AND should be 'active' when prereq parents are met", () => {
27 | cy.get(`[data-testid="and(csc209,csc258)"]`).contains("and")
28 | cy.get(`[data-testid="csc258"]`).click()
29 | cy.get(`[data-testid="csc209"]`).click()
30 | cy.get(`[data-testid="and(csc209,csc258)"]`).should("have.class", "active")
31 | cy.get(`[data-testid="csc367"]`).should("have.class", "takeable")
32 | })
33 | it("OR should be 'active' when 1+ of the prereq parents are met", () => {
34 | cy.get(`[data-testid="and(csc463,csc373)"]`).contains("or")
35 | cy.get(`[data-testid="csc373"]`).click()
36 | cy.get(`[data-testid="csc463"]`).click()
37 | cy.get(`[data-testid="and(csc463,csc373)"]`).should("have.class", "active")
38 | cy.get(`[data-testid="csc438"]`).should("have.class", "takeable")
39 | })
40 | })
41 |
42 | it("'missing' class when the mouse is hovering over a child class", () => {
43 | cy.get(`[data-testid="csc458"]`).trigger("mouseover")
44 | cy.get(`[data-testid="csc458"]`).should("have.class", "missing")
45 | cy.get(`[data-testid='h(csc263265)']`).should("have.class", "missing")
46 | cy.get(`[data-testid='and(csc209,csc258)']`).should("have.class", "missing")
47 | cy.get(`[data-testid='csc209']`).should("have.class", "missing")
48 | cy.get(`[data-testid='csc258']`).should("have.class", "missing")
49 | cy.get(`[data-testid='csc108']`).should("have.class", "missing")
50 |
51 | // unaffected, despite sharing the same prereqs as CSC458
52 | cy.get(`[data-testid='csc385']`).should("have.class", "inactive")
53 | })
54 |
55 | describe("Info Box", () => {
56 | it("clicking the info box should produce a pop up modal", () => {
57 | cy.visit("/graph")
58 | cy.contains("CSC108").trigger("mouseover")
59 | cy.contains("Info").click()
60 |
61 | cy.contains("CSC108 Introduction to Computer Programming")
62 | // force click because it's not visible
63 | cy.get(".ReactModal__Overlay").click({ force: true })
64 | })
65 | })
66 | })
67 |
--------------------------------------------------------------------------------
/eslint.config.cjs:
--------------------------------------------------------------------------------
1 | const { includeIgnoreFile } = require("@eslint/compat")
2 | const path = require("node:path")
3 | const globals = require("globals")
4 | const js = require("@eslint/js")
5 | const reactPlugin = require("eslint-plugin-react")
6 | const eslintPluginPrettierRecommended = require("eslint-plugin-prettier/recommended")
7 | const babelParser = require("@babel/eslint-parser")
8 |
9 | const ignorePath = path.resolve(__dirname, ".prettierignore")
10 |
11 | module.exports = [
12 | includeIgnoreFile(ignorePath),
13 | js.configs.recommended,
14 | reactPlugin.configs.flat.recommended,
15 | eslintPluginPrettierRecommended,
16 | {
17 | languageOptions: {
18 | globals: {
19 | ...globals.browser,
20 | ...globals.node,
21 | ...globals.jest,
22 | ...globals.es2015,
23 | $: "readonly",
24 | getURLParameter: "readonly",
25 | },
26 | parser: babelParser,
27 | },
28 | rules: {
29 | "no-console": "off",
30 | "react/no-find-dom-node": "off",
31 | "react/no-render-return-value": "off",
32 | "react/no-string-refs": "off",
33 | },
34 | settings: {
35 | react: {
36 | version: "detect",
37 | },
38 | },
39 | },
40 | ]
41 |
--------------------------------------------------------------------------------
/js/components/about/about.js:
--------------------------------------------------------------------------------
1 | import aboutContent from "../../../README.md"
2 |
3 | document.addEventListener("DOMContentLoaded", () => {
4 | const container = document.getElementById("aboutDiv")
5 | container.innerHTML = aboutContent
6 | })
7 |
--------------------------------------------------------------------------------
/js/components/common/Disclaimer.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | /**
4 | * A React component representing the disclaimer popup
5 | */
6 | export default class Disclaimer extends React.Component {
7 | constructor(props) {
8 | super(props)
9 | this.state = {
10 | hidden: localStorage.getItem("hide-disclaimer") === "true",
11 | }
12 | }
13 |
14 | handleClose = () => {
15 | this.setState({ hidden: true })
16 | }
17 |
18 | handleCheck = event => {
19 | if (event.target.checked) {
20 | localStorage.setItem("hide-disclaimer", "true")
21 | } else {
22 | localStorage.setItem("hide-disclaimer", "false")
23 | }
24 | }
25 |
26 | render() {
27 | const timetable = (
28 | Official Timetable
29 | )
30 | const calendar = (
31 | Academic Calendar
32 | )
33 |
34 | if (this.state.hidden) {
35 | return null
36 | } else {
37 | return (
38 |
39 |
42 |
43 |
Disclaimer
44 |
45 | Please make sure to confirm your course selections and prerequisites with
46 | official sources like the {timetable} and {calendar} as they are more
47 | reliable and up-to-date.{" "}
48 |
49 |
52 |
61 |
62 |
63 | )
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/js/components/common/__tests__/ConvertToLink.test.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { CourseModal } from "../react_modal.js.jsx"
3 |
4 | /**
5 | * Test for convertToLink.
6 | */
7 | describe("ConvertToLink", () => {
8 | let actual
9 | let content
10 | let expected
11 | const wrapper = new CourseModal({})
12 |
13 | describe("The content has no course names", () => {
14 | beforeEach(() => {
15 | content = "The application of logic and proof techniques."
16 | })
17 | test("The content is returned as all strings", () => {
18 | actual = wrapper.convertToLink(content)
19 | expected = [content]
20 | expect(actual).toEqual(expected)
21 | })
22 | })
23 |
24 | describe("The content has one course name", () => {
25 | beforeEach(() => {
26 | content = "Certain topics briefly mentioned in CSC165H1 may be covered."
27 | })
28 | test("The content is returned as one link tag and several strings", () => {
29 | actual = wrapper.convertToLink(content)
30 | expected = [
31 | "Certain topics briefly mentioned in ",
32 | this.clickCourseLink("CSC165H1")}
36 | >
37 | CSC165H1
38 | ,
39 | " may be covered.",
40 | ]
41 | expect(JSON.stringify(actual)).toEqual(JSON.stringify(expected))
42 | })
43 | })
44 |
45 | describe("The content has one course name followed by a symbol, such as ')' ", () => {
46 | beforeEach(() => {
47 | content = "(60% or higher in CSC111H1)"
48 | })
49 | test("The content is returned as one link tag and several strings \
50 | (including the symbol after the course name", () => {
51 | actual = wrapper.convertToLink(content)
52 | expected = [
53 | "(60% or higher in ",
54 | this.clickCourseLink("CSC111H1")}
58 | >
59 | CSC111H1
60 | ,
61 | ")",
62 | ]
63 | expect(JSON.stringify(actual)).toEqual(JSON.stringify(expected))
64 | })
65 | })
66 | })
67 |
--------------------------------------------------------------------------------
/js/components/common/__tests__/TimetableLoading.test.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { render, screen, cleanup } from "@testing-library/react"
3 | import { Description } from "../react_modal.js.jsx"
4 |
5 | describe("Displays correct content based on timetable availability", () => {
6 | beforeEach(() => cleanup())
7 |
8 | it("displays a timetable when there is only one session", async () => {
9 | const courseInfo = {
10 | course: {
11 | name: "CSC108H1",
12 | description: "sample description",
13 | prereqStr: [],
14 | distribution: null,
15 | breadth: "The Physical and Mathematical Universes (5)",
16 | },
17 | sessions: {
18 | F: [
19 | {
20 | activity: "LEC0101",
21 | availability: "50 out of 100 available",
22 | instructor: "Beyonce",
23 | room: ["BA "],
24 | time: ["Monday 13 - 14", "Wednesday 13 - 14", "Friday 13 - 14"],
25 | waitlist: "0 students",
26 | },
27 | ],
28 | S: [],
29 | Y: [],
30 | },
31 | }
32 |
33 | render()
34 | await screen.findByText(/CSC108H1/)
35 | await screen.findByText(/LEC0101/)
36 | await screen.findByText("sample description")
37 | await screen.findByText("Beyonce")
38 | })
39 |
40 | it("displays a timetable when there is more than one session", async () => {
41 | const courseInfo = {
42 | course: {
43 | name: "CSC108H1",
44 | description: "sample description",
45 | prereqStr: [],
46 | distribution: null,
47 | breadth: "The Physical and Mathematical Universes (5)",
48 | },
49 | sessions: {
50 | F: [
51 | {
52 | activity: "LEC0101",
53 | availability: "50 out of 100 available",
54 | instructor: "Beyonce",
55 | room: ["BA "],
56 | time: ["Monday 13 - 14", "Wednesday 13 - 14", "Friday 13 - 14"],
57 | waitlist: "0 students",
58 | },
59 | ],
60 | S: [
61 | {
62 | activity: "LEC0202",
63 | availability: "100 out of 200 available",
64 | instructor: "David. Liu",
65 | room: ["BA "],
66 | time: ["Monday 13 - 14", "Wednesday 13 - 14", "Friday 13 - 14"],
67 | waitlist: "0 students",
68 | },
69 | ],
70 | Y: [],
71 | },
72 | }
73 |
74 | render()
75 | await screen.findAllByText(/CSC108H1/)
76 | await screen.findByText(/LEC0101/)
77 | await screen.findByText(/LEC0202/)
78 | await screen.findByText("David. Liu")
79 | })
80 |
81 | it("displays reminder when there's no timetable information", async () => {
82 | const courseInfo = {
83 | course: {
84 | name: "CSC108H1",
85 | description: "sample description",
86 | prereqStr: [],
87 | distribution: null,
88 | breadth: "The Physical and Mathematical Universes (5)",
89 | },
90 | sessions: {
91 | F: [],
92 | S: [],
93 | Y: [],
94 | },
95 | }
96 |
97 | render()
98 | await screen.findByText("No timetable information available")
99 | })
100 | })
101 |
--------------------------------------------------------------------------------
/js/components/common/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Retrieves a course from file.
3 | * @param {string} courseName The course code. This + '.txt' is the name of the file.
4 | * @returns {Promise} Promise object representing the JSON object containing course information.
5 | */
6 | export function getCourse(courseName) {
7 | "use strict"
8 |
9 | return fetch("course?name=" + courseName)
10 | .then(response => response.json())
11 | .catch(error => {
12 | throw error
13 | })
14 | }
15 |
16 | /**
17 | * Retrieves a post from the server.
18 | * @param {string} postCode The post code on the art&sci timetable.
19 | * @param {string} lastModified The last time the client called this function in UTC time
20 | * @returns {Promise} Promise object representing the JSON object containing post information and
21 | * a boolean of whether the data was modified since last time
22 | */
23 | export function getPost(postCode, lastModified) {
24 | "use strict"
25 |
26 | return fetch("post?code=" + postCode, {
27 | headers: {
28 | "If-Modified-Since": lastModified,
29 | },
30 | })
31 | .then(async response => {
32 | if (response.status === 304) return { modified: false }
33 |
34 | const responseJson = await response.json()
35 |
36 | return {
37 | title: responseJson.postDepartment,
38 | description: responseJson.postDescription,
39 | requirements: responseJson.postRequirements,
40 | courseList: getCourseList(responseJson.postRequirements),
41 | modified: true,
42 | modifiedTime: response.headers.get("Last-modified"),
43 | }
44 | })
45 | .catch(error => {
46 | throw error
47 | })
48 | }
49 |
50 | /**
51 | * Parses the course codes from a requirement text and converts them to lower case
52 | * @param {string} requirements The requirement text containing the course codes
53 | * @returns {Array} Promise object representing an array of required or related courses.
54 | */
55 | function getCourseList(requirements) {
56 | const courseCodeRegex = /[A-Z]{3}[0-9]{3}(?=[HY][135])/g
57 | const courseList = requirements.match(courseCodeRegex) || []
58 | return courseList.map(course => course.toLowerCase())
59 | }
60 |
--------------------------------------------------------------------------------
/js/components/draw/main.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { createRoot } from 'react-dom/client'
3 | import { Graph } from "../graph/Graph"
4 |
5 | document.addEventListener("DOMContentLoaded", () => {
6 | const container = document.getElementById("react-graph")
7 | const root = createRoot(container)
8 | root.render()
9 | })
10 |
--------------------------------------------------------------------------------
/js/components/generate/generate.jsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from "react-dom/client"
2 | import GenerateForm from "./GenerateForm.js"
3 |
4 | const container = document.getElementById("generateRoot")
5 | const root = createRoot(container)
6 | root.render()
7 |
--------------------------------------------------------------------------------
/js/components/graph/Bool.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 |
4 | /** Function representing a boolean node (and/or) */
5 | export default function Bool(props) {
6 | const ellipseAttrs = {
7 | cx: props.JSON.pos[0],
8 | cy: props.JSON.pos[1],
9 | rx: "9.8800001",
10 | ry: "7.3684001",
11 | }
12 | return (
13 |
18 |
19 | {props.JSON.text.map(function (textTag, i) {
20 | const textAttrs = {
21 | x: ellipseAttrs.cx,
22 | y: textTag.pos[1],
23 | }
24 | return (
25 |
26 | {props.logicalType}
27 |
28 | )
29 | })}
30 |
31 | )
32 | }
33 |
34 | Bool.propTypes = {
35 | className: PropTypes.string,
36 | JSON: PropTypes.object,
37 | inEdges: PropTypes.array,
38 | logicalType: PropTypes.string,
39 | outEdges: PropTypes.array,
40 | parents: PropTypes.array,
41 | status: PropTypes.string,
42 | }
43 |
--------------------------------------------------------------------------------
/js/components/graph/Button.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 |
4 | export default function Button(props) {
5 | return (
6 |
18 | )
19 | }
20 |
21 | Button.propTypes = {
22 | divId: PropTypes.string,
23 | mouseDown: PropTypes.func,
24 | mouseUp: PropTypes.func,
25 | onMouseEnter: PropTypes.func,
26 | onMouseLeave: PropTypes.func,
27 | disabled: PropTypes.bool,
28 | text: PropTypes.string,
29 | children: PropTypes.node,
30 | }
31 |
--------------------------------------------------------------------------------
/js/components/graph/Container.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 | import Disclaimer from "../common/Disclaimer"
4 | import { Graph } from "./Graph"
5 | import FocusBar from "./FocusBar"
6 |
7 | export default class Container extends React.Component {
8 | constructor(props) {
9 | super(props)
10 | this.state = {
11 | currFocus: null,
12 | fceCount: 0,
13 | graphName: "",
14 | graphs: [],
15 | }
16 | this.graph = React.createRef()
17 | }
18 |
19 | UNSAFE_componentWillMount() {
20 | this.getLocalGraph()
21 | }
22 |
23 | componentDidMount() {
24 | fetch("/graphs")
25 | .then(res => res.json())
26 | .then(
27 | graphsData => {
28 | this.setState({
29 | graphs: graphsData.sort((a, b) => (a.id > b.id ? 1 : -1)),
30 | })
31 | },
32 | () => {
33 | throw "No graphs in database"
34 | }
35 | )
36 |
37 | // Enable "Export" link
38 | document.getElementById("nav-export")?.addEventListener("click", () => {
39 | this.graph.current.openExportModal()
40 | })
41 | }
42 |
43 | updateGraph = graphName => {
44 | this.setState({ graphName: graphName.replace("-", " ") })
45 | }
46 |
47 | getLocalGraph = () => {
48 | // Gets graph from local storage, if it exists
49 | let graphName
50 | const params = new URL(document.location).searchParams
51 | const urlSpecifiedGraph = params.get("dept")
52 |
53 | // HACK: Temporary workaround for giving the statistics department a
54 | // link to our graph.
55 | // Should be replaced with a more general solution.
56 | if (urlSpecifiedGraph === "sta") {
57 | graphName = "Statistics"
58 | } else if (urlSpecifiedGraph !== null) {
59 | graphName = "Computer Science"
60 | } else {
61 | graphName = localStorage.getItem("active-graph") || "Computer Science"
62 | }
63 | this.setState({ graphName: graphName })
64 | return graphName
65 | }
66 |
67 | setFCECount = credits => {
68 | this.setState({ fceCount: credits })
69 | }
70 |
71 | incrementFCECount = credits => {
72 | this.setState({ fceCount: this.state.fceCount + credits })
73 | }
74 |
75 | highlightFocus = id => {
76 | if (this.state.currFocus === id) {
77 | this.setState({
78 | currFocus: null,
79 | })
80 | } else {
81 | this.setState({
82 | currFocus: id,
83 | })
84 | }
85 | }
86 |
87 | render() {
88 | return (
89 |
90 |
91 |
105 |
110 |
111 | )
112 | }
113 | }
114 |
115 | Container.propTypes = {
116 | start_blank: PropTypes.bool,
117 | edit: PropTypes.bool,
118 | }
119 |
--------------------------------------------------------------------------------
/js/components/graph/Edge.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types"
2 | import React from "react"
3 |
4 | /**
5 | * Function-based component representing an edge from a Node/Bool to a Node/Bool
6 | */
7 | export default function Edge(props) {
8 | let pathDescription = "M"
9 | props.points.forEach(p => {
10 | pathDescription += p[0] + "," + p[1] + " "
11 | })
12 |
13 | return (
14 | ${props.target}`}
19 | markerEnd="url(#arrowHead)"
20 | />
21 | )
22 | }
23 |
24 | Edge.propTypes = {
25 | className: PropTypes.string,
26 | /** Array of points for the edge. A straight edge will have 2. Each turn in the edge means another point*/
27 | points: PropTypes.array,
28 | /** Node from which the edge is drawn*/
29 | source: PropTypes.string,
30 | /** Node that the edge is pointing to */
31 | target: PropTypes.string,
32 | /** Status of this edge */
33 | status: PropTypes.string,
34 | /** Transform of this edge */
35 | transform: PropTypes.string,
36 | }
37 |
--------------------------------------------------------------------------------
/js/components/graph/FocusBar.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 | import FocusTab from "./FocusTab.js"
4 |
5 | // These lists are in reverse order to what ends up appearing on the screen
6 | const computerScienceFocusLabels = [
7 | ["ASFOC1689J", "Web Technologies"],
8 | ["ASFOC1689I", "Theory of Computation"],
9 | ["ASFOC1689A", "Scientific Computing"],
10 | ["ASFOC1689H", "Human\u2011Computer Interaction"],
11 | ["ASFOC1689G", "Game Design"],
12 | ["ASFOC1689D", "Computer Vision"],
13 | ["ASFOC1689F", "Computer Systems"],
14 | ["ASFOC1689C", "Computational Linguistics"],
15 | ["ASFOC1689B", "Artificial Intelligence"],
16 | ]
17 |
18 | /**
19 | * React component representing the focus menu bar
20 | */
21 | export default class FocusBar extends React.Component {
22 | constructor(props) {
23 | super(props)
24 | this.state = {
25 | open: false,
26 | }
27 | }
28 |
29 | /**
30 | * Changes whether the focus bar is open or not
31 | */
32 | toggleFocusBar = () => {
33 | this.setState({ open: !this.state.open })
34 | }
35 |
36 | /**
37 | * Creates the menu items of the focus bar using the FocusTab component
38 | * @returns an array of FocusTab components
39 | */
40 | generateFocusTabs = () => {
41 | return computerScienceFocusLabels.map(([focusId, focusTitle]) => {
42 | const selected = this.props.currFocus === focusId
43 |
44 | return (
45 |
52 | )
53 | })
54 | }
55 |
56 | render() {
57 | if (!this.props.focusBarEnabled) {
58 | return null
59 | } else {
60 | return (
61 |