├── .dockerignore ├── .eslintrc.json ├── .github ├── CODEOWNERS └── workflows │ ├── npm.yml │ └── php.yml ├── .gitignore ├── .htaccess ├── .nvmrc ├── Dockerfile ├── LICENSE ├── README.md ├── apache-config.conf ├── api ├── entity.php ├── generate.php ├── img.php ├── rmp.php ├── schedule.php ├── search.php ├── src │ ├── Generate.php │ ├── S3Manager.php │ └── Schedule.php └── status.php ├── assets ├── prod │ └── .gitkeep └── src │ └── modules │ └── sm │ ├── App │ ├── controllers │ │ └── AppController.ts │ ├── directives │ │ ├── courseCartDirective.ts │ │ ├── courseDetailPopover.ts │ │ ├── dowSelectFieldsDirective.ts │ │ ├── loadingButtonDirective.ts │ │ ├── navCloseOnMobileDirective.ts │ │ ├── paginationControlsDirective.ts │ │ ├── pinnedDirective.ts │ │ ├── professorLookupDirective.ts │ │ └── selectTermDirective.ts │ ├── providers │ │ ├── RMPUrlFilter.ts │ │ ├── cartFilterFilter.ts │ │ ├── codeOrNumberFilter.ts │ │ ├── courseNumFilter.ts │ │ ├── entityDataRequestFactory.ts │ │ ├── formatTimeFilter.ts │ │ ├── globalKbdShortcutsFactory.ts │ │ ├── localStorageFactory.ts │ │ ├── openPopupFactory.ts │ │ ├── parseSectionTimesFilter.ts │ │ ├── parseTimeFilter.ts │ │ ├── partitionFilter.ts │ │ ├── shareServiceInfoFactory.ts │ │ ├── startFromFilter.ts │ │ ├── translateDayFilter.ts │ │ └── uiDayFactoryFactory.ts │ ├── styles │ │ ├── bootstrap.css │ │ └── global.css │ └── templates │ │ ├── 404.html │ │ ├── cart.html │ │ └── help.html │ ├── Browse │ ├── controllers │ │ └── BrowseController.ts │ ├── directives │ │ └── browseListDirective.ts │ └── templates │ │ └── browse.html │ ├── Generate │ ├── controllers │ │ ├── GenerateController.ts │ │ ├── GenerateNoScheduleCoursesController.ts │ │ ├── GenerateNonScheduleCoursesController.ts │ │ └── GenerateScheduleCoursesController.ts │ ├── directives │ │ ├── dynamicItemDirective.ts │ │ ├── dynamicItemsDirective.ts │ │ └── scheduleCourseDirective.ts │ └── templates │ │ ├── courseselect.html │ │ └── generate.html │ ├── Index │ └── templates │ │ └── index.html │ ├── Schedule │ ├── controllers │ │ ├── ScheduleController.ts │ │ ├── SchedulePrintController.ts │ │ └── ScheduleViewController.ts │ ├── directives │ │ ├── scheduleActionsDirective.ts │ │ ├── scheduleDirective.ts │ │ └── svgTextLineDirective.ts │ ├── providers │ │ └── reloadScheduleFactory.ts │ └── templates │ │ ├── schedule.html │ │ ├── schedule.print.html │ │ ├── schedule.view.html │ │ └── scheduleitem.html │ ├── Search │ ├── controllers │ │ └── SearchController.ts │ └── templates │ │ └── search.html │ ├── Status │ ├── controllers │ │ └── StatusController.ts │ └── templates │ │ └── status.html │ ├── app.ts │ └── global.d.ts ├── composer.json ├── favicon.ico ├── gulpfile.js ├── img ├── csh_logo_square.svg ├── csh_og.png └── csh_print.png ├── inc ├── ajaxError.php ├── config.env.php ├── config.example.php ├── databaseConn.php ├── stringFunctions.php └── timeFunctions.php ├── index.php ├── manifest.json ├── package.json ├── processDump.Dockerfile ├── robots.txt ├── schema ├── eer_diagram.pdf ├── migrationScripts │ ├── 1.2.1to1.3-BldgCodes.php │ ├── 1.2.1to1.3-ScheduleQuarters.php │ ├── 1.4to1.5-innodb.sql │ ├── 1.5.1to1.6.sql │ ├── 1.6.4to1.6.5.sql │ ├── 1.7.1to1.8.sql │ ├── 3.0.30.sql │ └── f_4charcourses.sql ├── procedures │ ├── InsertOrUpdateCourse.sql │ └── InsertOrUpdateSection.sql ├── schema_model.mwb └── tables │ ├── buildings.sql │ ├── courses.sql │ ├── departments.sql │ ├── quarters.sql │ ├── schedulecourses.sql │ ├── schedulenoncourses.sql │ ├── schedules.sql │ ├── schools.sql │ ├── scrapelog.sql │ ├── sections.sql │ └── times.sql ├── tools ├── Parser.php ├── processDump.php ├── pruneSchedules.php ├── truncateCourses.sql └── truncateSchedules.sql └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | .gitignore 4 | .nvmrc 5 | node_modules 6 | assets/dist 7 | assets/prod 8 | LICENSE 9 | README.md 10 | 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "extends": [ 12 | "standard" 13 | ], 14 | "globals": { 15 | "Atomics": "readonly", 16 | "SharedArrayBuffer": "readonly" 17 | }, 18 | "parserOptions": { 19 | "ecmaVersion": 2018 20 | }, 21 | "rules": { 22 | "no-undef": "off", 23 | "no-var": "warn", 24 | "no-unused-vars": "warn", 25 | "no-prototype-builtins": "warn", 26 | "no-redeclare": "warn", 27 | "no-inner-declarations": "warn", 28 | "no-throw-literal": "warn" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ComputerScienceHouse/schedulemaker-maintainers 2 | -------------------------------------------------------------------------------- /.github/workflows/npm.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Checks 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | pull_request: 7 | branches: [ master, develop ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [12.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm install 25 | - run: npm run lint 26 | - run: npm run typecheck 27 | - run: npm run build 28 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Checks 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | pull_request: 7 | branches: [ master, develop ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Validate composer.json and composer.lock 18 | run: composer validate 19 | 20 | - name: Install dependencies 21 | run: composer install --prefer-dist --no-progress --no-suggest --ignore-platform-reqs 22 | 23 | - name: Check PHP Syntax 24 | run: if find . -name "*.php" ! -path "./vendor/*" -exec php -l {} 2>&1 \; | grep "syntax error, unexpected"; then exit 1; fi 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #### PROJECT IGNORES #### 2 | 3 | # Ignore configuration files 4 | inc/config.php 5 | 6 | # Ignore schedule images 7 | img/schedules/*.png 8 | 9 | # Ignore node_modules dependencies 10 | node_modules/* 11 | package-lock.json 12 | 13 | # Ignore built files 14 | assets/prod/* 15 | assets/dist/* 16 | !assets/prod/.gitkeep 17 | 18 | # Other files 19 | _Local Working Files/* 20 | 21 | #### EDITOR IGNORES #### 22 | 23 | # Ignore PhpStorm files 24 | .idea 25 | *.iml 26 | 27 | # Ignore Eclipse files 28 | .settings 29 | .project 30 | .buildpath 31 | 32 | # Ignore VisualStudio files 33 | *.phpproj* 34 | *.sln 35 | *.suo 36 | web.config 37 | vwd.webinfo 38 | WebEssentials-Settings.json 39 | vendor 40 | composer.lock 41 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | # COMPRESSION 2 | AddOutputFilterByType DEFLATE text/text text/html text/plain text/xml text/css application/x-javascript application/javascript application/json 3 | 4 | 5 | # REWRITES 6 | 7 | RewriteEngine On 8 | 9 | # Let the old ScheduleMaker handle Resig's schedules for now 10 | #RewriteCond %{QUERY_STRING} ^mode=old&id=(.*)$ [NC] 11 | RewriteRule ^schedule.php$ http://schedule-old.csh.rit.edu/schedule.php [NC,L,R=302] 12 | 13 | # Legacy Rewrites from previous version 14 | RewriteCond %{QUERY_STRING} ^id=(.*)$ [NC] 15 | RewriteRule ^schedule.php$ /schedule/%1? [NC,L,R=302] 16 | RewriteRule ^(generate|roulette|search|status).php$ /$1 [R=302,L] 17 | 18 | # Rewrite any request that wants json to the api directory 19 | RewriteCond %{HTTP:Accept} application/json [NC] 20 | RewriteRule ^(schedule|generate|entity|search|status|rmp)(?:/([^/]*))*$ api/$1.php [L] 21 | RewriteRule ^schedule/[^/]*/ical$ api/schedule.php [L] 22 | RewriteRule ^img/schedules/[^/]*.png$ api/img.php [L] 23 | 24 | # Don't rewrite files or directories 25 | RewriteCond %{REQUEST_FILENAME} -f [OR] 26 | RewriteCond %{REQUEST_FILENAME} -d 27 | RewriteRule ^ - [L] 28 | 29 | # Rewrite everything else to index.html to allow html5 state links 30 | RewriteRule ^ index.php [L] 31 | 32 | 33 | # CACHING 34 | 35 | # Set max-age one year in the future for all static assets 36 | # Allow caching for all assets 37 | 38 | Header set Cache-Control "max-age=29030400, public" 39 | Header unset ETag 40 | FileETag None 41 | ExpiresActive On 42 | ExpiresDefault A29030400 43 | 44 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/node:12-buster-slim as builder 2 | LABEL author="Devin Matte " 3 | 4 | WORKDIR /usr/src/schedule 5 | COPY package.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY package.json tsconfig.json gulpfile.js ./ 10 | COPY assets ./assets 11 | RUN npm run-script build 12 | 13 | 14 | FROM docker.io/php:7.3-apache 15 | LABEL author="Devin Matte " 16 | 17 | RUN echo "deb-src http://deb.debian.org/debian buster main" >> /etc/apt/sources.list 18 | 19 | RUN apt-get -yq update && \ 20 | apt-get -yq install \ 21 | gnupg \ 22 | libmagickwand-dev \ 23 | git \ 24 | gcc \ 25 | make \ 26 | autoconf \ 27 | libc-dev \ 28 | pkg-config \ 29 | build-essential \ 30 | libx11-dev \ 31 | libxext-dev \ 32 | zlib1g-dev \ 33 | libpng-dev \ 34 | libjpeg-dev \ 35 | libfreetype6-dev \ 36 | libxml2-dev \ 37 | unzip \ 38 | wget \ 39 | --no-install-recommends 40 | 41 | RUN apt-get -yq build-dep imagemagick 42 | 43 | RUN wget https://github.com/ImageMagick/ImageMagick6/archive/6.9.11-22.tar.gz && \ 44 | tar -xzvf 6.9.11-22.tar.gz && \ 45 | cd ImageMagick6-6.9.11-22 && \ 46 | ./configure && \ 47 | make && \ 48 | make install && \ 49 | ldconfig /usr/local/lib && \ 50 | make check 51 | 52 | RUN docker-php-ext-install mysqli && \ 53 | yes '' | pecl install imagick && docker-php-ext-enable imagick 54 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 55 | 56 | COPY apache-config.conf /etc/apache2/sites-enabled/000-default.conf 57 | 58 | RUN a2enmod rewrite && a2enmod headers && a2enmod expires && \ 59 | sed -i '/Listen/{s/\([0-9]\+\)/8080/; :a;n; ba}' /etc/apache2/ports.conf && \ 60 | chmod og+rwx /var/lock/apache2 && chmod -R og+rwx /var/run/apache2 61 | 62 | COPY . /var/www/html 63 | COPY --from=builder /usr/src/schedule/assets/prod /var/www/html/assets/prod 64 | 65 | RUN composer install 66 | 67 | EXPOSE 8080 68 | EXPOSE 8443 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | CSH Schedule**Maker** 4 | ===================== 5 | 6 | ![GPL-2.0](https://img.shields.io/github/license/computersciencehouse/schedulemaker.svg) 7 | ![PHP Version](https://img.shields.io/badge/php-7.3-blue) 8 | 9 | A course database lookup tool and schedule building web application for use at Rochester Institute of Technology. 10 | Built, maintained and hosted by [Computer Science House](https://csh.rit.edu) 11 | 12 | Available at [schedule.csh.rit.edu](https://schedule.csh.rit.edu) 13 | 14 |
15 | 16 | ## Dev Environment 17 | 18 | ### Setup 19 | - Fork and clone the repository. 20 | - Copy the `config.example.php` file to `config.php` in `/inc/` or set environment variables as defined in `config.env.php`. 21 | - Contact a current maintainer for server/database configs. 22 | - If you wish to see images locally, you will also need S3 credentials, either supply your own or reach out. 23 | 24 | ### Run Locally 25 | In order to run locally youre going to need [docker](https://www.docker.com/). 26 | 27 | ``` 28 | docker build -t schedulemaker . 29 | docker run --rm -i -t -p 5000:8080 --name=schedulemaker schedulemaker 30 | ``` 31 | 32 | You can replace `5000` with whatever port you wish to connect locally to. Then visit `http://localhost:5000` in a browser. 33 | 34 | ### Development 35 | - To build js files run `npm run-script build`. 36 | - Increment the version number in `package.json` after updating js/css files or ensure all cache cleared. 37 | - Make sure you increment at least the patch number in any PRs that touch Javascript/CSS. 38 | - If you get an error with JS/CSS not loading, check your config file. 39 | 40 | ## Maintainers 41 | 42 | ### Current Maintainers 43 | - Devin Matte ([@devinmatte](https://github.com/devinmatte)) 44 | 45 | #### Past Maintainers 46 | - Ben Grawi ([@bgrawi](https://github.com/bgrawi)) 47 | - Benjamin Russell ([@benrr101](https://github.com/benrr101)) 48 | -------------------------------------------------------------------------------- /apache-config.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerAdmin webmaster@csh.rit.edu 3 | DocumentRoot /var/www/html 4 | 5 | 6 | Options Indexes FollowSymLinks MultiViews 7 | AllowOverride All 8 | Order deny,allow 9 | Allow from all 10 | 11 | 12 | ErrorLog ${APACHE_LOG_DIR}/error.log 13 | CustomLog ${APACHE_LOG_DIR}/access.log combined 14 | 15 | 16 | -------------------------------------------------------------------------------- /api/img.php: -------------------------------------------------------------------------------- 1 | returnImage($id); 33 | -------------------------------------------------------------------------------- /api/rmp.php: -------------------------------------------------------------------------------- 1 | "query TeacherSearchResultsPageQuery(\n \$query: TeacherSearchQuery!\n \$schoolID: ID\n \$includeSchoolFilter: Boolean!\n) {\n search: newSearch {\n ...TeacherSearchPagination_search_1ZLmLD\n }\n school: node(id: \$schoolID) @include(if: \$includeSchoolFilter) {\n __typename\n ... on School {\n name\n }\n id\n }\n}\n\nfragment TeacherSearchPagination_search_1ZLmLD on newSearch {\n teachers(query: \$query, first: 8, after: \"\") {\n didFallback\n edges {\n cursor\n node {\n ...TeacherCard_teacher\n id\n __typename\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n resultCount\n filters {\n field\n options {\n value\n id\n }\n }\n }\n}\n\nfragment TeacherCard_teacher on Teacher {\n id\n legacyId\n avgRating\n numRatings\n ...CardFeedback_teacher\n ...CardSchool_teacher\n ...CardName_teacher\n ...TeacherBookmark_teacher\n}\n\nfragment CardFeedback_teacher on Teacher {\n wouldTakeAgainPercent\n avgDifficulty\n}\n\nfragment CardSchool_teacher on Teacher {\n department\n school {\n name\n id\n }\n}\n\nfragment CardName_teacher on Teacher {\n firstName\n lastName\n}\n\nfragment TeacherBookmark_teacher on Teacher {\n id\n isSaved\n}\n", 21 | "variables" => [ 22 | "query" => [ 23 | "text" => $name, 24 | "schoolID" => "U2Nob29sLTgwNw==", 25 | "fallback" => false, // No results from other schools 26 | "departmentID" => null, 27 | ], 28 | "schoolID" => "U2Nob29sLTgwNw==", 29 | "includeSchoolFilter" => true 30 | ], 31 | ]); 32 | curl_setopt($curl, CURLOPT_POSTFIELDS, $payload); 33 | 34 | curl_setopt($curl, CURLOPT_URL, "https://www.ratemyprofessors.com/graphql"); 35 | echo curl_exec($curl); 36 | -------------------------------------------------------------------------------- /api/schedule.php: -------------------------------------------------------------------------------- 1 | getScheduleFromId($id); 58 | 59 | // Set header for ical mime, output the xml 60 | header("Content-Type: text/calendar"); 61 | header("Content-Disposition: attachment; filename=generated_schedule" . md5(serialize($schedule)) . ".ics"); 62 | echo $scheduleApi->generateIcal($schedule); 63 | 64 | break; 65 | 66 | case "old": 67 | echo json_encode(["error" => "Not supported on this platform. Please use http://schedule-old.csh.rit.edu/"]); 68 | break; 69 | 70 | 71 | case "schedule": 72 | // JSON DATA STRUCTURE ///////////////////////////////////////////// 73 | // We're outputting json, so use that 74 | header('Content-type: application/json'); 75 | 76 | // Required parameters 77 | if (empty($id)) { 78 | die(json_encode(["error" => true, "msg" => "You must provide a schedule"])); 79 | } 80 | 81 | // Pull the schedule and output it as json 82 | $schedule = $scheduleApi->getScheduleFromId($id); 83 | if ($schedule == null) { 84 | echo json_encode([ 85 | 'error' => true, 86 | 'msg' => 'Schedule not found' 87 | ]); 88 | } else { 89 | echo json_encode($schedule); 90 | } 91 | 92 | break; 93 | 94 | //////////////////////////////////////////////////////////////////////// 95 | // STORE A SCHEDULE 96 | case "save": 97 | // There has to be a json object given 98 | if (empty($_POST['data'])) { 99 | die(json_encode(["error" => "argument", "msg" => "No schedule was provided", "arg" => "schedule"])); 100 | } 101 | // This will be raw data since there is no more sanatize like there used to be in the old code 102 | $json = $_POST['data']; 103 | 104 | // Make sure the object was successfully decoded 105 | $json = sanitize(json_decode($json, true)); 106 | if ($json == null) { 107 | die(json_encode(["error" => "argument", "msg" => "The schedule could not be decoded", "arg" => "schedule"])); 108 | } 109 | if (!isset($json['starttime']) || !isset($json['endtime']) || !isset($json['building']) || !isset($json['startday']) 110 | || !isset($json['endday']) 111 | ) { 112 | die(json_encode(["error" => "argument", "msg" => "A required schedule parameter was not provided"])); 113 | } 114 | 115 | // Start the storing process with storing the data about the schedule 116 | $query = "INSERT INTO schedules (oldid, startday, endday, starttime, endtime, building, quarter)" . 117 | " VALUES('', '{$json['startday']}', '{$json['endday']}', '{$json['starttime']}', '{$json['endtime']}', '{$json['building']}', " 118 | . 119 | " '{$json['term']}')"; 120 | $result = $dbConn->query($query); 121 | if (!$result) { 122 | die(json_encode(["error" => "mysql", "msg" => "Failed to store the schedule: " . $dbConn->error])); 123 | } 124 | 125 | // Grab the latest id for the schedule 126 | $schedId = $dbConn->insert_id; 127 | 128 | // Optionally process the svg for the schedule 129 | $image = false; 130 | if (!empty($_POST['svg']) && $scheduleApi->renderSvg($_POST['svg'], $schedId)) { 131 | $query = "UPDATE schedules SET image = ((1)) WHERE id = '{$schedId}'"; 132 | $dbConn->query($query); // We don't particularly care if this fails 133 | } 134 | 135 | // Now iterate through the schedule 136 | foreach ($json['schedule'] as $item) { 137 | // Process it into schedulenoncourses if the item is a non-course item 138 | if ($item['courseNum'] == "non") { 139 | // Process each time as a seperate item 140 | foreach ($item['times'] as $time) { 141 | $query = "INSERT INTO schedulenoncourses (title, day, start, end, schedule)" . 142 | " VALUES('{$item['title']}', '{$time['day']}', '{$time['start']}', '{$time['end']}', '{$schedId}')"; 143 | $result = $dbConn->query($query); 144 | if (!$result) { 145 | die(json_encode([ 146 | "error" => "mysql", 147 | "msg" => "Storing non-course item '{$item['title']}' failed: " . $dbConn->error 148 | ])); 149 | } 150 | } 151 | } else { 152 | // Process each course. It's crazy simple now. 153 | $query = "INSERT INTO schedulecourses (schedule, section)" . 154 | " VALUES('{$schedId}', '{$item['id']}')"; 155 | $result = $dbConn->query($query); 156 | if (!$result) { 157 | die(json_encode(["error" => "mysql", "msg" => "Storing a course '{$item['courseNum']}' failed: " . $dbConn->erorr])); 158 | } 159 | } 160 | } 161 | 162 | // Everything was successful, return a nice, simple URL to the schedule 163 | // To make it cool, let's make it a hex id 164 | $hexId = dechex($schedId); 165 | $url = "{$HTTPROOTADDRESS}schedule/{$hexId}"; 166 | 167 | echo json_encode(["url" => $url, "id" => $hexId]); 168 | 169 | break; 170 | 171 | default: 172 | // INVALID OPTION ////////////////////////////////////////////////// 173 | die(json_encode(["error" => "argument", "msg" => "You must provide a valid action."])); 174 | break; 175 | } 176 | -------------------------------------------------------------------------------- /api/search.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @file api/search.php 10 | * @descrip Provides standalone JSON object retreival via ajax for the course roulette page 11 | ****************************************************************************/ 12 | 13 | // FUNCTIONS /////////////////////////////////////////////////////////////// 14 | 15 | /** 16 | * Halts execution if the provided variable is not numeric and it isn't null. 17 | * This also dumps a nice jSON encoded error message 18 | * 19 | * @param mixed $var The variable we're asserting is numeric 20 | * @param string $name The name of the variable to include in the error 21 | * message. Also the name of the argument. 22 | * 23 | * @return void 24 | */ 25 | function assertNumeric($var, $name): void { 26 | if (!is_numeric($var) && !is_null($var)) { 27 | die(json_encode(["error" => "argument", "msg" => "You must provide a valid {$name}!", "arg" => $name])); 28 | } 29 | } 30 | 31 | // REQUIRED FILES ////////////////////////////////////////////////////////// 32 | if (file_exists('../inc/config.php')) { 33 | include_once "../inc/config.php"; 34 | } else { 35 | include_once "../inc/config.env.php"; 36 | } 37 | require_once "../inc/databaseConn.php"; 38 | require_once "../inc/timeFunctions.php"; 39 | require_once "../inc/ajaxError.php"; 40 | 41 | // HEADERS ///////////////////////////////////////////////////////////////// 42 | header("Content-type: application/json"); 43 | 44 | // POST PROCESSING ///////////////////////////////////////////////////////// 45 | $_POST = sanitize($_POST); 46 | 47 | // MAIN EXECUTION ////////////////////////////////////////////////////////// 48 | 49 | // We're providing JSON 50 | header('Content-type: application/json'); 51 | 52 | switch (getAction()) { 53 | case "find": 54 | // Check that the required fields are provided 55 | $term = $_POST['term']; 56 | if (empty($term)) { 57 | // We cannot continue! 58 | echo json_encode(["error" => "argument", "msg" => "You must provide a term", "arg" => "term"]); 59 | } 60 | 61 | // Term, school, department, credits, times-any, times, professor 62 | // School and department will be empty strings if any OR not selected 63 | $school = (!empty($_POST['college']) && $_POST['college'] != 'any') ? $_POST['college'] : null; 64 | $credits = (!empty($_POST['credits'])) ? $_POST['credits'] : null; 65 | $professor = (!empty($_POST['professor'])) ? $_POST['professor'] : null; 66 | $title = (!empty($_POST['title'])) ? $_POST['title'] : null; 67 | $description = (!empty($_POST['description'])) ? explode(',', $_POST['description']) : []; 68 | $level = (!empty($_POST['level']) && $_POST['level'] != 'any') ? $_POST['level'] : null; 69 | if (!empty($_POST['department']) && $_POST['department'] != 'any') { 70 | $department = $_POST['department']; 71 | $school = null; // We won't search for a school if department is assigned. 72 | } else { 73 | $department = null; 74 | } 75 | 76 | if (count($description) > 0) { 77 | $keyword_SQL = 'AND ('; 78 | foreach ($description as $keyword) { 79 | $keyword = trim($keyword); 80 | $keyword_SQL .= "c.description LIKE '%{$keyword}%' OR "; 81 | } 82 | $keyword_SQL = substr($keyword_SQL, 0, -4) . ")"; 83 | } else { 84 | $keyword_SQL = null; 85 | } 86 | 87 | // Validate the numerical arguments we got 88 | assertNumeric($term, "term"); 89 | assertNumeric($school, "school"); 90 | assertNumeric($credits, "number of credits"); 91 | 92 | // Times (Search for any if any is selected, search for the specified times, OR don't specify any time data) 93 | if (!empty($_POST['timesAny']) && $_POST['timesAny'] == 'any') { 94 | $times = null; 95 | $timesAny = true; 96 | } elseif (empty($_POST['timesAny']) && !empty($_POST['times'])) { 97 | $times = (is_array($_POST['times'])) ? $_POST['times'] : [$_POST['times']]; 98 | $timesAny = false; 99 | } else { 100 | $times = null; 101 | $timesAny = true; 102 | } 103 | // Days (same process as the time) 104 | if (!empty($_POST['daysAny']) && $_POST['daysAny'] == 'any') { 105 | $days = null; 106 | $daysAny = true; 107 | } elseif (empty($_POST['daysAny']) && !empty($_POST['days'])) { 108 | $days = (is_array($_POST['days'])) ? $_POST['days'] : [$_POST['days']]; 109 | $daysAny = false; 110 | } else { 111 | $days = null; 112 | $daysAny = true; 113 | } 114 | 115 | // Build the query 116 | $query = "SELECT s.id"; 117 | $query .= " FROM courses AS c "; 118 | $query .= " JOIN sections AS s ON s.course = c.id"; 119 | $query .= " JOIN departments AS d ON d.id = c.department"; 120 | $query .= " WHERE quarter = '{$term}'"; 121 | $query .= " AND s.status != 'X'"; 122 | $query .= ($school) ? " AND d.school = '{$school}'" : ""; 123 | $query .= ($department) ? " AND c.department = '{$department}'" : ""; 124 | $query .= ($credits) ? " AND c.credits = '{$credits}'" : ""; 125 | $query .= ($professor) ? " AND s.instructor LIKE '%{$professor}%'" : ""; 126 | $query .= ($title) ? " AND c.title LIKE '%{$title}%'" : ""; 127 | $query .= ($keyword_SQL) ? $keyword_SQL : ""; 128 | if ($level) { // Process the course level 129 | if ($level == 'beg') { 130 | $query .= " AND c.course < 300"; 131 | } 132 | if ($level == 'int') { 133 | $query .= " AND c.course >= 300 AND c.course < 600"; 134 | } 135 | if ($level == 'grad') { 136 | $query .= " AND c.course >= 600"; 137 | } 138 | } 139 | $timeConstraints = []; 140 | if ($times) { // Process the time constraints 141 | $timequery = []; 142 | if (in_array("morn", $times)) { 143 | $timequery[] = "(start >= 480 AND start < 720)"; 144 | } 145 | if (in_array("aftn", $times)) { 146 | $timequery[] = "(start >= 720 AND start < 1020)"; 147 | } 148 | if (in_array("even", $times)) { 149 | $timequery[] = "(start > 1020)"; 150 | } 151 | $timeConstraints[] = "(" . implode(" OR ", $timequery) . ")"; // Make it a single string (condition OR condition ...) 152 | } 153 | if ($days) { // Process the day constraints 154 | $dayquery = []; 155 | foreach ($days as $day) { 156 | $dayquery[] = "day = " . translateDay($day); 157 | } 158 | $timeConstraints[] = "(" . implode(" OR ", $dayquery) . ")"; // Do the same as we did with the times 159 | } 160 | if (count($timeConstraints)) { 161 | // Now cram the two together into one concise subquery 162 | $query .= " AND s.id IN (SELECT section FROM times WHERE " . implode(" AND ", $timeConstraints) . ")"; 163 | } 164 | 165 | // Run it! 166 | $result = $dbConn->query($query); 167 | if (!$result) { 168 | echo json_encode(["error" => "mysql", "msg" => "An error occurred while searching the database."]); 169 | break; 170 | } 171 | if ($result->num_rows == 0) { 172 | echo json_encode(["error" => "result", "msg" => "No courses matched your criteria"]); 173 | break; 174 | } 175 | 176 | // Now we build an array of the results 177 | $courses = []; 178 | while ($row = $result->fetch_assoc()) { 179 | $courses[] = $row['id']; 180 | } 181 | // @todo: store this in session to avoid lengthy and costly queries 182 | 183 | // Loop through all results and fill them out 184 | $matchingCourses = []; 185 | foreach ($courses as $sectionId) { 186 | 187 | // Look up the course 188 | $course = getCourseBySectionId($sectionId); 189 | 190 | // Do we need to exclude it because it's online? 191 | if ($course['online'] == true && (!isset($_POST['online']) || $_POST['online'] != true)) { 192 | // Yes, it should be excluded 193 | continue; 194 | } 195 | 196 | // Determine if its on campus 197 | $offCampus = false; 198 | if (!empty($course['times'])) { 199 | foreach ($course['times'] as $time) { 200 | if ($time['off_campus']) { 201 | $offCampus = true; 202 | break; 203 | } 204 | } 205 | } 206 | 207 | // Do we need to exclude this course? 208 | if ((isset($_POST['offCampus']) && $_POST['offCampus'] == 'true') || !$offCampus) { 209 | // No need to exclude it -- match found 210 | $matchingCourses[] = $course; 211 | } 212 | } 213 | 214 | // Courses will be empty if there are no results 215 | if (count($matchingCourses) == 0) { 216 | echo json_encode(["error" => "result", "msg" => "No courses matched your criteria"]); 217 | } else { 218 | echo json_encode($matchingCourses); 219 | } 220 | break; 221 | 222 | default: 223 | echo json_encode(["error" => "argument", "msg" => "Invalid or no action provided", "arg" => "action"]); 224 | break; 225 | } 226 | -------------------------------------------------------------------------------- /api/src/S3Manager.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class S3Manager 16 | { 17 | 18 | private $s3Client; 19 | private $imageBucket; 20 | 21 | function __construct($S3_KEY, $S3_SECRET, $S3_SERVER, $S3_IMAGE_BUCKET) { 22 | $this->s3Client = new S3Client([ 23 | 'region' => 'us-east-1', 24 | 'version' => '2006-03-01', 25 | 'endpoint' => $S3_SERVER, 26 | 'credentials' => [ 27 | 'key' => $S3_KEY, 28 | 'secret' => $S3_SECRET 29 | ], 30 | 'use_path_style_endpoint' => true 31 | ]); 32 | $this->imageBucket = $S3_IMAGE_BUCKET; 33 | 34 | //Creating S3 Bucket 35 | try { 36 | $this->s3Client->createBucket([ 37 | 'Bucket' => $S3_IMAGE_BUCKET, 38 | ]); 39 | } catch (AwsException $e) { 40 | // output error message if fails 41 | echo $e->getMessage(); 42 | echo "\n"; 43 | } 44 | } 45 | 46 | public function saveImage(Imagick $im, $id) { 47 | //TODO: Error Handling 48 | $this->s3Client->putObject([ 49 | 'Bucket' => $this->imageBucket, 50 | 'ContentType' => 'image/png', 51 | 'Key' => "{$id}.png", 52 | 'Body' => $im->getImageBlob() 53 | ]); 54 | } 55 | 56 | public function getImage(string $id) { 57 | return $this->s3Client->getObject([ 58 | 'Bucket' => $this->imageBucket, 59 | 'ContentType' => 'image/png', 60 | 'Key' => "{$id}.png", 61 | ]); 62 | } 63 | 64 | public function returnImage(string $id) { 65 | $result = $this->getImage($id); 66 | 67 | return $result['Body']; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /api/status.php: -------------------------------------------------------------------------------- 1 | query($query); 55 | $lastLogs = []; 56 | while ($row = $result->fetch_assoc()) { 57 | $lastLogs[] = $row; 58 | } 59 | echo json_encode($lastLogs); 60 | -------------------------------------------------------------------------------- /assets/prod/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ComputerScienceHouse/schedulemaker/52f58d92c9dc3ee245a48fe9a78319c081a23b6a/assets/prod/.gitkeep -------------------------------------------------------------------------------- /assets/src/modules/sm/App/directives/courseCartDirective.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').directive('courseCart', function () { 2 | return { 3 | restrict: 'A', 4 | templateUrl: '/<%=modulePath%>App/templates/cart.min.html' 5 | } 6 | }) 7 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/directives/courseDetailPopover.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').directive('courseDetailPopover', function ($http, $filter) { 2 | const RMPUrl = $filter('RMPUrl') 3 | const parseTimes = $filter('parseSectionTimes') 4 | const formatTime = $filter('formatTime') 5 | 6 | function getTimesHTML (times) { 7 | if (!times) { 8 | return '' 9 | } 10 | const parsedTimes = parseTimes(times, true) 11 | let HTML = '
' 12 | for (let timeIndex = 0; timeIndex < parsedTimes.length; timeIndex++) { 13 | const time = parsedTimes[timeIndex] 14 | HTML += time.days + ' ' + formatTime(time.start) + '-' + formatTime(time.end) + ' Location: ' + time.location + '' 15 | if (timeIndex < parsedTimes.length - 1) { 16 | HTML += '
' 17 | } 18 | } 19 | HTML += '
' 20 | 21 | return HTML 22 | } 23 | 24 | return { 25 | restrict: 'A', 26 | scope: { 27 | sectionId: '=courseDetailPopover' 28 | }, 29 | link: function (scope, elm) { 30 | if (scope.sectionId !== '') { 31 | let loaded = false 32 | let opened = false 33 | const $body = $('body') 34 | 35 | function hidePopoverOnBodyClick () { 36 | setTimeout(function () { 37 | $body.off('click.hidepopovers') 38 | $body.on('click.hidepopovers', function () { 39 | elm.popover('destroy') 40 | loaded = false 41 | $body.off('click.hidepopovers') 42 | opened = false 43 | }) 44 | }, 100) 45 | } 46 | 47 | elm.on('click', function () { 48 | if (!loaded) { 49 | loaded = true 50 | $http.post('/entity/courseForSection', 51 | $.param({ 52 | id: scope.sectionId 53 | }) 54 | ).success(function (data) { 55 | elm.popover({ 56 | html: true, 57 | trigger: 'click', 58 | 59 | title: data.courseNum, 60 | content: '
' + data.curenroll + '/' + data.maxenroll + '

' + data.title + ' - ' + data.credits + ' credits
' + RMPUrl(data.instructor) + '

' + getTimesHTML(data.times) + '

' + data.description + '

', 61 | container: '#container' 62 | }) 63 | elm.popover('show') 64 | opened = true 65 | hidePopoverOnBodyClick() 66 | }).error(function () { 67 | loaded = false 68 | }) 69 | } else { 70 | // elm.popover('toggle'); 71 | opened = !opened 72 | if (opened) { 73 | // hidePopoverOnBodyClick(); 74 | } 75 | } 76 | }) 77 | } 78 | } 79 | } 80 | }) 81 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/directives/dowSelectFieldsDirective.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').directive('dowSelectFields', function (uiDayFactory) { 2 | return { 3 | restrict: 'A', 4 | scope: { 5 | select: '=dowSelectFields' 6 | }, 7 | template: '
', 8 | link: { 9 | pre: function (scope) { 10 | scope.days = uiDayFactory() 11 | scope.isSelected = function (day) { 12 | return scope.select.indexOf(day) !== -1 13 | } 14 | scope.toggle = function (day) { 15 | const index = scope.select.indexOf(day) 16 | if (index === -1) { 17 | scope.select.push(day) 18 | } else { 19 | scope.select.splice(index, 1) 20 | } 21 | } 22 | } 23 | } 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/directives/loadingButtonDirective.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').directive('loadingButton', function (uiDayFactory) { 2 | const template = ' ' 3 | 4 | return { 5 | restrict: 'A', 6 | scope: { 7 | status: '=loadingButton', 8 | text: '@loadingText' 9 | }, 10 | link: function (scope, elm) { 11 | const prevHTML = elm.html() 12 | scope.$watch('status', function (newLoading, prevLoading) { 13 | if (newLoading !== prevLoading) { 14 | if (newLoading === 'L') { 15 | elm.html(template + scope.text) 16 | elm.attr('disabled', true) 17 | } else { 18 | elm.html(prevHTML) 19 | elm.attr('disabled', false) 20 | } 21 | } 22 | }) 23 | } 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/directives/navCloseOnMobileDirective.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').directive('navCloseOnMobile', function () { 2 | return { 3 | restrict: 'A', 4 | link: function (scope, elm) { 5 | const nav = $(elm) 6 | $(elm).find('li').click(function () { 7 | ($('.navbar-collapse.in') as any).collapse('hide') 8 | }) 9 | } 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/directives/paginationControlsDirective.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').directive('paginationControls', function () { 2 | return { 3 | restrict: 'A', 4 | scope: { 5 | displayOptions: '=paginationControls', 6 | totalLength: '=paginationLength', 7 | paginationCallback: '&' 8 | }, 9 | template: '' + 10 | ' {{displayOptions.currentPage+1}}/{{numberOfPages()}} ' + 11 | '', 12 | link: { 13 | pre: function (scope) { 14 | scope.numberOfPages = function () { 15 | const numPages = Math.ceil(scope.totalLength / scope.displayOptions.pageSize) 16 | if (scope.displayOptions.currentPage === numPages) { 17 | scope.displayOptions.currentPage = numPages - 1 18 | } 19 | return numPages 20 | } 21 | }, 22 | post: function (scope, elm, attrs) { 23 | if (scope.paginationCallback) { 24 | elm.find('button').click(function () { 25 | scope.paginationCallback() 26 | }) 27 | } 28 | } 29 | } 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/directives/pinnedDirective.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').directive('pinned', function () { 2 | return { 3 | restrict: 'A', 4 | link: function (scope, elm, attrs) { 5 | const $window = $(window) 6 | const sizer = elm.parent().parent().find('.pinned-sizer') 7 | const $footer = $('footer.main') 8 | let fO; let sO 9 | const updateHeight = function () { 10 | fO = $window.height() - $footer.offset().top - $footer.outerHeight() 11 | sO = sizer.height() 12 | elm.css('height', (fO > 0) ? (sO - fO) : (sO)) 13 | } 14 | setTimeout(function () { 15 | updateHeight() 16 | $(window).on('resize', updateHeight) 17 | }, 100) 18 | 19 | if (typeof scope.schools !== 'undefined') { 20 | scope.$watch('schools', function () { 21 | setTimeout(updateHeight, 200) 22 | }) 23 | } 24 | 25 | elm.addClass('pinned') 26 | } 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/directives/professorLookupDirective.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').directive('professorLookup', function ($http) { 2 | return { 3 | restrict: 'A', 4 | scope: { 5 | professorLookup: '=' 6 | }, 7 | template: '{{professorLookup}}', 8 | link: { 9 | pre: function (scope, elm, attrs) {}, 10 | post: function (scope, elm, attrs) { 11 | if (scope.professorLookup !== '' && scope.professorLookup !== 'TBA') { 12 | scope.stats = 'none' 13 | elm.on('click', function () { 14 | if (scope.stats === 'none') { 15 | $http 16 | .post( 17 | '/api/rmp.php', 18 | { name: scope.professorLookup }, 19 | { 20 | headers: { 21 | 'Content-Type': 'application/json' 22 | } 23 | } 24 | ) 25 | .success(function (data, status, headers, config) { 26 | const results = data.data.search.teachers.edges 27 | if (!results[0]) { 28 | elm.popover({ 29 | html: true, 30 | trigger: 'manual', 31 | placement: 'auto left', 32 | title: scope.professorLookup, 33 | content: 'No results on RateMyProfessors.com!' 36 | }) 37 | elm.popover('show') 38 | scope.stats = null 39 | return 40 | } 41 | const teacher = results[0].node 42 | const ratingColor = function (score) { 43 | score = parseFloat(score) 44 | if (score >= 4) { 45 | return '#18BC9C' 46 | } else if (score >= 3) { 47 | return '#F39C12' 48 | } else { 49 | return '#E74C3C' 50 | } 51 | } 52 | scope.stats = { 53 | name: teacher.firstName + ' ' + teacher.lastName, 54 | url: 55 | 'https://www.ratemyprofessors.com/professor/' + 56 | teacher.legacyId, 57 | dept: teacher.department, 58 | numRatings: teacher.numRatings, 59 | rating: teacher.avgRating, 60 | difficulty: teacher.avgDifficulty 61 | } 62 | const yearNumber = new Date().getFullYear() 63 | elm.popover({ 64 | html: true, 65 | trigger: 'manual', 66 | placement: 'auto left', 67 | title: 68 | '' + 71 | scope.stats.name + 72 | ' - ' + 73 | scope.stats.dept + 74 | '', 75 | content: 76 | '

' + 79 | scope.stats.rating + 80 | '

Average Rating

' + 83 | scope.stats.difficulty + 84 | '

Level of Difficulty
Based on ' + 85 | scope.stats.numRatings + 86 | ' ratings
Not the right professor?
© ${yearNumber} RateMyProfessors.com
` 89 | }) 90 | elm.popover('show') 91 | }) 92 | } else { 93 | elm.popover('toggle') 94 | } 95 | }) 96 | } 97 | } 98 | } 99 | } 100 | }) 101 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/directives/selectTermDirective.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').directive('selectTerm', function () { 2 | return { 3 | restrict: 'A', 4 | template: '' 5 | } 6 | }) 7 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/RMPUrlFilter.ts: -------------------------------------------------------------------------------- 1 | // For now, not a service 2 | angular.module('sm').filter('RMPUrl', function () { 3 | return function (input: string) { 4 | if (input && input !== 'TBA') { 5 | const EscapedName = encodeURIComponent(input) 6 | return ( 7 | '' + 10 | input + 11 | '' 12 | ) 13 | } else { 14 | return '' + input + '' 15 | } 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/cartFilterFilter.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').filter('cartFilter', function () { 2 | return function (input, $scope) { 3 | const parsed = [] 4 | const SSFN = $scope.courseCart.count.course.selectedSections 5 | angular.forEach(input, function (course: Course) { 6 | if (course && course.sections.length > 0 && !course.sections[0].isError && SSFN(course) > 0) { 7 | parsed.push(course) 8 | } 9 | }) 10 | return parsed 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/codeOrNumberFilter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return either the code or number if it set 3 | */ 4 | angular.module('sm').filter('codeOrNumber', function () { 5 | return function (input) { 6 | return (input.code) ? input.code : input.number 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/courseNumFilter.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').filter('formatTime', function () { 2 | return function (minutes) { 3 | minutes = minutes % 1440 4 | 5 | // Figure out how many hours 6 | let hours = Math.floor(minutes / 60) 7 | 8 | // Figure out how many minutes 9 | let remMinutes: string | number = minutes % 60 10 | 11 | // Correct for AM/PM 12 | let ampm 13 | if (hours >= 12) { 14 | ampm = 'pm' 15 | hours -= 12 16 | } else { 17 | ampm = 'am' 18 | } 19 | 20 | // Correct for 0 hour 21 | if (hours === 0) { 22 | hours = 12 23 | } 24 | 25 | // Correct minutes less than 10 min 26 | if (remMinutes < 10) { 27 | remMinutes = '0' + remMinutes 28 | } 29 | // Put it together 30 | return hours + ':' + remMinutes + ampm 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/entityDataRequestFactory.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').factory('entityDataRequest', function ($http) { 2 | const entityDataRequest = function (params, callback?) { 3 | return $http.post('/entity/' + params.action, $.param(params)) 4 | } 5 | return { 6 | getSchoolsForTerm: function (opts) { 7 | return entityDataRequest({ 8 | action: 'getSchoolsForTerm', 9 | term: opts.term 10 | }) 11 | }, 12 | getDepartmentsForSchool: function (opts) { 13 | return entityDataRequest({ 14 | action: 'getDepartments', 15 | term: opts.term, 16 | school: opts.param 17 | }) 18 | }, 19 | getCoursesForDepartment: function (opts) { 20 | return entityDataRequest({ 21 | action: 'getCourses', 22 | term: opts.term, 23 | department: opts.param 24 | }) 25 | }, 26 | getSectionsForCourse: function (opts) { 27 | return entityDataRequest({ 28 | action: 'getSections', 29 | course: opts.param 30 | }) 31 | } 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/formatTimeFilter.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').filter('courseNum', function () { 2 | return function (course: Course) { 3 | if (course) { 4 | const coursePrefix = course.department.code ? course.department.code : course.department.number 5 | return coursePrefix + '-' + course.course 6 | } 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/globalKbdShortcutsFactory.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').factory('globalKbdShortcuts', function ($rootScope) { 2 | const globalKbdShortcuts = { 3 | bindCtrlEnter: function (callback) { 4 | Mousetrap.bind('mod+enter', function (e) { 5 | $rootScope.$apply(callback) 6 | return true 7 | }) 8 | 9 | // Only allow to bind once, so mock function after first use 10 | this.bindCtrlEnter = function () { 11 | } 12 | }, 13 | bindEnter: function (callback) { 14 | Mousetrap.bind('enter', function (e) { 15 | $rootScope.$apply(callback) 16 | return true 17 | }) 18 | 19 | // Only allow to bind once, so mock function after first use 20 | this.bindCtrlEnter = function () { 21 | } 22 | }, 23 | bindPagination: function (callback) { 24 | Mousetrap.bind('mod+right', function (e) { 25 | $rootScope.$apply(callback.apply(e)) 26 | return true 27 | }) 28 | Mousetrap.bind('mod+left', function (e) { 29 | $rootScope.$apply(callback.apply(e)) 30 | return true 31 | }) 32 | 33 | // Only allow to bind once, so mock function after first use 34 | this.bindPagination = function () { 35 | } 36 | }, 37 | bindSelectCourses: function (callback) { 38 | Mousetrap.bind('mod+down', function (e) { 39 | callback() 40 | return false 41 | }) 42 | 43 | // Only allow to bind once, so mock function after first use 44 | this.bindSelectCourses = function () { 45 | } 46 | } 47 | } 48 | return globalKbdShortcuts 49 | }) 50 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/localStorageFactory.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').factory('localStorage', function ($window) { 2 | const localStorage = $window.localStorage 3 | 4 | return { 5 | setItem: function (key: string, value) { 6 | if (localStorage) { 7 | if (value != null) { 8 | localStorage.setItem(key, angular.toJson(value)) 9 | } else { 10 | localStorage.setItem(key, null) 11 | } 12 | } else { 13 | return false 14 | } 15 | }, 16 | getItem: function (key: string) { 17 | if (localStorage) { 18 | return angular.fromJson(localStorage.getItem(key)) 19 | } else { 20 | return false 21 | } 22 | }, 23 | hasKey: function (key: string) { 24 | if (localStorage) { 25 | return localStorage.hasOwnProperty(key) 26 | } else { 27 | return false 28 | } 29 | }, 30 | clear: function () { 31 | if (localStorage) { 32 | return localStorage.clear() 33 | } else { 34 | return false 35 | } 36 | } 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/openPopupFactory.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').factory('openPopup', function ($window) { 2 | /** 3 | * A utility to get top/left with a given width and height 4 | */ 5 | const getPosition = function (width: number, height: number) { 6 | // Set defaults if either not set 7 | if (!width || !height) { 8 | width = 550 9 | height = 450 10 | } 11 | 12 | // Return an object and calculate correct position 13 | return { 14 | width: width, 15 | height: height, 16 | top: Math.round((screen.height / 2) - (height / 2)), 17 | left: Math.round((screen.width / 2) - (width / 2)) 18 | } 19 | } 20 | 21 | return function (width, height) { 22 | const settings = ['about:blank'] 23 | 24 | if (width !== true) { 25 | const pos = getPosition(width, height) 26 | settings.push('Loading...') 27 | settings.push('left=' + pos.left + 28 | ',top=' + pos.top + 29 | ',width=' + pos.width + 30 | ',height=' + pos.height + 31 | ',personalbar=0,toolbar=0,scrollbars=1,resizable=1') 32 | } else { 33 | settings.push('_blank') 34 | } 35 | 36 | return $window.open.apply($window, settings) 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/parseSectionTimesFilter.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').filter('parseSectionTimes', function ($filter) { 2 | const translateDay = $filter('translateDay') 3 | return function (times, byLocation) { 4 | if (typeof times !== 'object') return times 5 | const parsedTimes = [] 6 | for (let e = 0; e < times.length; ++e) { 7 | // Search the existing list of times to see if a match exists 8 | let found: number | boolean = false 9 | const time = times[e] 10 | 11 | if (byLocation && typeof time.bldg !== 'undefined') { 12 | time.location = time.bldg.code + 13 | '(' + time.bldg.number + ')' + 14 | '-' + time.room 15 | } else { 16 | time.location = false 17 | } 18 | 19 | for (let f = 0; f < parsedTimes.length; ++f) { 20 | if (parsedTimes[f].start === time.start && parsedTimes[f].end === time.end && parsedTimes[f].location === time.location) { 21 | found = f 22 | } 23 | } 24 | 25 | // If a match was found, add the day to it, otherwise add a new time 26 | if (found !== false) { 27 | parsedTimes[found].days += ', ' + translateDay(time.day) 28 | } else { 29 | parsedTimes.push({ 30 | start: time.start, 31 | end: time.end, 32 | days: translateDay(time.day), 33 | location: time.location 34 | }) 35 | } 36 | } 37 | return parsedTimes 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/parseTimeFilter.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').filter('parseTime', function () { 2 | return function (rawTime) { 3 | const matchedTime = rawTime.match(/([0-9]|1[0-2]):([0-9]{2})(am|pm)/) 4 | if (matchedTime) { 5 | if (matchedTime[3] === 'am' && parseInt(matchedTime[1]) === 12) { 6 | return parseInt(matchedTime[2]) 7 | } else if (matchedTime[3] === 'pm') { 8 | matchedTime[1] = parseInt(matchedTime[1]) + 12 9 | } 10 | return (parseInt(matchedTime[1]) * 60) + parseInt(matchedTime[2]) 11 | } else { 12 | return false 13 | } 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/partitionFilter.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').filter('partition', function ($cacheFactory) { 2 | const arrayCache = $cacheFactory('partition') 3 | return function (arr, size) { 4 | const parts = [] 5 | const jsonArr = JSON.stringify(arr) 6 | for (let i = 0; i < arr.length; i += size) { 7 | parts.push(arr.slice(i, i + size)) 8 | } 9 | const cachedParts = arrayCache.get(jsonArr) 10 | if (JSON.stringify(cachedParts) === JSON.stringify(parts)) { 11 | return cachedParts 12 | } 13 | arrayCache.put(jsonArr, parts) 14 | 15 | return parts 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/shareServiceInfoFactory.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').factory('shareServiceInfo', function () { 2 | // Define the services and their common functions 3 | return { 4 | twitter: function (url) { 5 | return 'http://twitter.com/share?url=' + encodeURIComponent(url) + '&text=My%20Class%20Schedule' 6 | }, 7 | facebook: function (url) { 8 | return 'http://www.facebook.com/sharer.php?u=' + encodeURIComponent(url) 9 | } 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/startFromFilter.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').filter('startFrom', function () { 2 | return function (input, start: number) { 3 | start = +start // parse to int 4 | return input.slice(start) 5 | } 6 | }) 7 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/translateDayFilter.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').filter('translateDay', function () { 2 | return function (day: number) { 3 | // Modulo it to make sure we get the correct days 4 | day = day % 7 5 | 6 | // Now switch on the different days 7 | switch (day) { 8 | case 0: 9 | return 'Sun' 10 | case 1: 11 | return 'Mon' 12 | case 2: 13 | return 'Tue' 14 | case 3: 15 | return 'Wed' 16 | case 4: 17 | return 'Thu' 18 | case 5: 19 | return 'Fri' 20 | case 6: 21 | return 'Sat' 22 | default: 23 | return null 24 | } 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/providers/uiDayFactoryFactory.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').factory('uiDayFactory', function () { 2 | const days: string[] = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] 3 | 4 | return function () { 5 | return days 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/templates/404.html: -------------------------------------------------------------------------------- 1 |
2 |
404 Error. The page you were looking for was not found. Go back to the homepage.
3 |
4 | -------------------------------------------------------------------------------- /assets/src/modules/sm/App/templates/cart.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | 13 |

Course Cart 14 | 17 |

18 | 19 |
20 |
21 |
22 |
    23 |
  • 24 |
    25 | 28 | 31 |
    32 |

    {{course.search}}:

    33 |

    {{courseCart.count.course.selectedSections(course)}} selected

    34 |
      35 |
    • 36 | 39 |

      {{section.courseNum?section.courseNum:(course | courseNum)+'-'+section.section}}

      40 |

      41 | 44 |
    • 45 |
    46 |
  • 47 |
48 |
Add courses to your cart and make a schedule with them. They will show up here.
49 |
50 |
51 | 54 |
55 |
-------------------------------------------------------------------------------- /assets/src/modules/sm/App/templates/help.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Help

5 |
6 |
7 |

How to Use

8 |

Use the "Make a Schedule" page to enter in courses by their course number to find them. All results from each search are added to your course cart automatically. You can manually remove classes if you want. You can also separate different courses by including a comma in the select field. The resulting schedules will contain EITHER course, never both.

9 |

You can use the "Browse Courses" page to see all available courses and sections for any given term. This is useful for seeing what type of classes are available to take. You can add courses and sections to the course cart that will also be used to generate your schedules.

10 |

The new "Course Search" page allows you to search through titles and descriptions, as well as a variety of other parameters. You can add any of your results to your cart and make a schedule with them as well!

11 |

Keyboard Shortcuts

12 | Make a Schedule Page 13 |
    14 |
  • Ctrl + Enter: Generate schedules
  • 15 |
  • Ctrl + Up or Down: Move between course search fields
  • 16 |
  • Ctrl + Alt + Down: Toggle display of search results 17 |
  • Ctrl + Alt + 1-9: Toggle selection of course result by index
  • 18 |
  • Ctrl + Left or Right: Move between pages of schedules
  • 19 |
20 | Search Page 21 |
    22 |
  • Enter: Search
  • 23 |
  • Ctrl + Left or Right: Move between pages of results
  • 24 |
25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Browse/controllers/BrowseController.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').controller('BrowseController', function ($scope, entityDataRequest) { 2 | $scope.schools = [] 3 | 4 | $scope.$watch('state.requestOptions.term', function (newTerm) { 5 | entityDataRequest.getSchoolsForTerm({ term: newTerm }).success(function (data, status) { 6 | if (status === 200 && typeof data.error === 'undefined') { 7 | $scope.schools = data 8 | } else if (data.error) { 9 | // TODO: Better error checking 10 | alert(data.msg) 11 | } 12 | }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Browse/directives/browseListDirective.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').directive('browseList', function ($http, entityDataRequest) { 2 | const hierarchy = ['school', 'department', 'course', 'section'] 3 | const capitalize = function (string) { 4 | return string.charAt(0).toUpperCase() + string.slice(1) 5 | } 6 | 7 | return { 8 | restrict: 'A', 9 | link: { 10 | pre: function (scope, elm, attrs) { 11 | const hIndex = hierarchy.indexOf(attrs.browseList) 12 | if (hIndex === -1) { 13 | throw 'browseList mode does not exist' 14 | } 15 | const itemName = hierarchy[hIndex] 16 | const childrenName = hierarchy[hIndex + 1] + 's' 17 | const entityDataRequestMethodName = 'get' + capitalize(childrenName) + 'For' + capitalize(itemName) 18 | scope[itemName][childrenName] = [] 19 | scope[itemName].ui = { 20 | expanded: false, 21 | buttonClass: 'fa-plus', 22 | toggleDisplay: function () { 23 | scope[itemName].ui.expanded = !scope[itemName].ui.expanded 24 | 25 | if (scope[itemName].ui.expanded && scope[itemName][childrenName].length === 0) { 26 | scope[itemName].ui.loading = true 27 | scope[itemName].ui.buttonClass = 'fa-refresh fa-spin' 28 | if (itemName === 'course') { 29 | if (scope.courseCart.contains.course(scope.course)) { 30 | let sections = [] 31 | for (let i = 0; i < scope.state.courses.length; i++) { 32 | if (scope.course.id === scope.state.courses[i].id) { 33 | sections = scope.state.courses[i].sections 34 | break 35 | } 36 | } 37 | if (sections.length > 0) { 38 | scope.course.sections = sections 39 | scope[itemName].ui.buttonClass = 'fa-minus' 40 | return 41 | } 42 | } 43 | } 44 | entityDataRequest[entityDataRequestMethodName]({ 45 | term: scope.state.requestOptions.term, 46 | param: scope[itemName].id 47 | }).success(function (data, status) { 48 | if (status === 200 && typeof data.error === 'undefined') { 49 | if (data[childrenName].length > 0) { 50 | scope[itemName][childrenName] = data[childrenName] 51 | } else { 52 | scope[itemName].ui.noResults = true 53 | } 54 | } else if (data.error) { 55 | // TODO: Better error checking 56 | alert(data.msg) 57 | } 58 | scope[itemName].ui.buttonClass = 'fa-minus' 59 | }) 60 | } else if (scope[itemName].ui.expanded) { 61 | scope[itemName].ui.buttonClass = 'fa-minus' 62 | } else { 63 | scope[itemName].ui.buttonClass = 'fa-plus' 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | }) 71 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Browse/templates/browse.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | Once you've found some courses you like, simply add them to your cart, they will be included in your possible schedules. Also, check out the help page for more info. 7 |
8 |
9 |
10 |
11 |
12 |

Browse Courses

13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |

28 | 29 |

30 |
31 |
32 |
33 | 36 |

{{school | codeOrNumber}}

37 |

{{school.title?school.title:" "}}

38 |
39 |
40 |
41 |
42 |
43 | 46 |

{{department.code?department.code:department.number}}

47 |

{{department.title?department.title:" "}}

48 |
49 |
50 |
51 |
52 |
53 | 56 |

{{course | courseNum}}

57 |

{{course.title}}

58 |
59 |
60 | 61 |

{{course.description}}

62 |
63 | 67 |
68 |
69 |
70 |
71 |
72 |
    73 |
  • 74 |
    75 |
    76 |

    {{$index + 1}}. {{course | courseNum}}-{{section.section}}

    77 | {{section.title}} 78 |

    79 | 80 |

    81 |
    82 |
    83 | {{time.days}} {{time.start | formatTime}}-{{time.end | formatTime}} Location: {{time.location}} 84 |
    85 |
    86 |
    87 |
    88 |
    89 |
    90 | 93 |
    94 |
    95 |
    96 |
    97 |
  • 98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
No courses available
107 |
108 |
109 |
110 |
111 |
112 |
No departments available
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | 123 | 126 |
127 |
128 |
129 |
130 | 133 | 136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
-------------------------------------------------------------------------------- /assets/src/modules/sm/Generate/controllers/GenerateController.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').controller('GenerateController', function ($scope, globalKbdShortcuts, $http, $filter, localStorage, uiDayFactory) { 2 | // Check if we are forking a schedule 3 | if (localStorage.hasKey('forkSchedule')) { 4 | // Get the schedule from sessions storage 5 | const forkSchedule = localStorage.getItem('forkSchedule') 6 | if (forkSchedule != null) { 7 | // Clear it so we don't fork again 8 | localStorage.setItem('forkSchedule', null) 9 | 10 | const days = uiDayFactory() 11 | 12 | // Init state, but save UI settings 13 | const savedUI = $scope.state.ui 14 | $scope.initState() 15 | $scope.state.ui = savedUI 16 | 17 | // Reload, then null term 18 | if ($scope.state.ui.temp_savedScheduleTerm) { 19 | $scope.state.requestOptions.term = +$scope.state.ui.temp_savedScheduleTerm 20 | $scope.state.ui.temp_savedScheduleTerm = null 21 | } 22 | 23 | for (let i = forkSchedule.length; i--;) { 24 | const course: Course = forkSchedule[i] 25 | 26 | // If it's a real course 27 | if (course.courseNum !== 'non') { 28 | $scope.courseCart.create.fromExistingScheduleCourse(course) 29 | } else { 30 | // Make a non-course item 31 | const nonCourse = { 32 | title: course.title, 33 | days: [days[parseInt(course.times[0].day)]], 34 | startTime: parseInt(course.times[0].start), 35 | endTime: parseInt(course.times[0].end) 36 | } 37 | let mergedNonCourse: boolean = false 38 | 39 | // Try to merge this non course with other similar ones 40 | for (let n = 0, l = $scope.state.nonCourses.length; n < l; n++) { 41 | const otherNonCourse = $scope.state.nonCourses[n] 42 | if (otherNonCourse.title === nonCourse.title && 43 | otherNonCourse.startTime === nonCourse.startTime && 44 | otherNonCourse.endTime === nonCourse.endTime) { 45 | otherNonCourse.days = otherNonCourse.days.concat(nonCourse.days) 46 | mergedNonCourse = true 47 | break 48 | } 49 | } 50 | 51 | if (!mergedNonCourse) { 52 | $scope.state.nonCourses.push(nonCourse) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | // Decorate some course helpers for our dynamic items directive 60 | $scope.courses_helpers = { 61 | add: $scope.courseCart.create.blankCourse, 62 | remove: function (index: number) { 63 | $scope.courseCart.remove.byIndex(index - 1) 64 | if ($scope.state.courses.length === 0) { 65 | $scope.courses_helpers.add() 66 | } 67 | } 68 | } 69 | 70 | $scope.ensureCorrectEndDay = function () { 71 | if ($scope.state.drawOptions.startDay > $scope.state.drawOptions.endDay) { 72 | $scope.state.drawOptions.endDay = $scope.state.drawOptions.startDay 73 | } 74 | } 75 | $scope.ensureCorrectEndTime = function () { 76 | if ($scope.state.drawOptions.startTime >= $scope.state.drawOptions.endTime) { 77 | $scope.state.drawOptions.endTime = $scope.state.drawOptions.startTime + 60 78 | } 79 | } 80 | 81 | $scope.numberOfPages = function () { 82 | return Math.ceil($scope.state.schedules.length / $scope.state.displayOptions.pageSize) 83 | } 84 | 85 | $scope.scrollToSchedules = function () { 86 | // I know this is bad, but I'm lazy 87 | setTimeout(function () { 88 | $('input:focus').blur() 89 | $('html, body').animate({ 90 | scrollTop: $('#master_schedule_results').offset().top - 65 91 | }, 500) 92 | }, 100) 93 | } 94 | 95 | $scope.generationStatus = 'D' 96 | 97 | // Overwrite app-level generateController 98 | $scope.generateSchedules = function () { 99 | $scope.generationStatus = 'L' 100 | 101 | const requestData = { 102 | term: $scope.state.requestOptions.term, 103 | courseCount: $scope.state.courses.length, 104 | nonCourseCount: $scope.state.nonCourses.length, 105 | noCourseCount: $scope.state.noCourses.length 106 | } 107 | 108 | // Set the actual number of courses being sent 109 | let actualCourseIndex: number = 1 110 | 111 | // Loop through the course cart 112 | for (let courseIndex = 0; courseIndex < $scope.state.courses.length; courseIndex++) { 113 | // Set up our variables 114 | const course = $scope.state.courses[courseIndex] 115 | const fieldName = 'courses' + (actualCourseIndex) + 'Opt[]' 116 | requestData['courses' + actualCourseIndex] = course.search 117 | requestData[fieldName] = [] 118 | let sectionCount: number = 0 119 | 120 | // Add selected sections to the request 121 | for (let sectionIndex = 0; sectionIndex < course.sections.length; sectionIndex++) { 122 | if (course.sections[sectionIndex].selected) { 123 | requestData[fieldName].push(course.sections[sectionIndex].id) 124 | sectionCount++ 125 | } 126 | } 127 | 128 | // If no sections are selected, remove the course info and decrease the actual course index 129 | if (sectionCount === 0) { 130 | requestData.courseCount-- 131 | delete requestData['courses' + actualCourseIndex] 132 | delete requestData[fieldName] 133 | } else { 134 | actualCourseIndex++ 135 | } 136 | } 137 | 138 | // Set the request data for the non courses 139 | for (let nonCourseIndex = 0; nonCourseIndex < $scope.state.nonCourses.length; nonCourseIndex++) { 140 | const nonCourse = $scope.state.nonCourses[nonCourseIndex] 141 | const index: number = (nonCourseIndex + 1) 142 | const fieldName = 'nonCourse' 143 | requestData[fieldName + 'Title' + index] = nonCourse.title 144 | requestData[fieldName + 'StartTime' + index] = nonCourse.startTime 145 | requestData[fieldName + 'EndTime' + index] = nonCourse.endTime 146 | requestData[fieldName + 'Days' + index + '[]'] = nonCourse.days 147 | } 148 | 149 | // Set the request data for the no courses stuff 150 | for (let noCourseIndex = 0; noCourseIndex < $scope.state.noCourses.length; noCourseIndex++) { 151 | const noCourse = $scope.state.noCourses[noCourseIndex] 152 | const index = (noCourseIndex + 1) 153 | const fieldName = 'noCourse' 154 | requestData[fieldName + 'StartTime' + index] = noCourse.startTime 155 | requestData[fieldName + 'EndTime' + index] = noCourse.endTime 156 | requestData[fieldName + 'Days' + index + '[]'] = noCourse.days 157 | } 158 | 159 | // Actually make the request 160 | $http.post('/generate/getMatchingSchedules', $.param(requestData)) 161 | .success(function (data, status, headers, config) { 162 | ga('send', 'event', 'generate', 'schedule') 163 | window.DD_RUM && 164 | window.DD_RUM.addAction('Generate', { 165 | type: 'Schedule', 166 | data: data 167 | }) 168 | 169 | $scope.generationStatus = 'D' 170 | 171 | // If no errors happened 172 | if (!data.error && !data.errors) { 173 | // Check if any schedules were generated 174 | if (data.schedules === undefined || data.schedules == null || data.schedules.length === 0) { 175 | $scope.resultError = 'There are no matching schedules!' 176 | } else { 177 | // Otherwise reset page, scroll to schedules and clear errors 178 | $scope.state.displayOptions.currentPage = 0 179 | $scope.scrollToSchedules() 180 | 181 | for (let count = 0; count < data.schedules.length; count++) { 182 | data.schedules[count][0].initialIndex = count 183 | } 184 | 185 | $scope.state.schedules = data.schedules 186 | $scope.resultError = '' 187 | } 188 | } else if (!data.error && data.errors) { 189 | // Display errors 190 | $scope.resultError = data.errors.reduce(function (totals, error) { 191 | return totals + ', ' + error.msg 192 | }, '') 193 | console.log('Schedule Generation Errors:', data) 194 | } else { 195 | // Display errors 196 | $scope.resultError = data.msg 197 | console.log('Schedule Generation Error:', data) 198 | } 199 | }) 200 | .error(function (data, status, headers, config) { 201 | $scope.generationStatus = 'D' 202 | // Display errors 203 | $scope.resultError = 'Fatal Error: An internal server error occurred' 204 | console.log('Fatal Schedule Generation Error:', data) 205 | }) 206 | } 207 | 208 | // Bind keyboard shortcuts 209 | globalKbdShortcuts.bindCtrlEnter($scope.generateSchedules) 210 | 211 | // Bind arrow key pagination 212 | globalKbdShortcuts.bindPagination(function () { 213 | if (this.keyCode === 39 && $scope.state.displayOptions.currentPage + 1 < $scope.numberOfPages()) { 214 | $scope.state.displayOptions.currentPage++ 215 | $scope.scrollToSchedules() 216 | } else if (this.keyCode === 37 && $scope.state.displayOptions.currentPage - 1 >= 0) { 217 | $scope.state.displayOptions.currentPage-- 218 | $scope.scrollToSchedules() 219 | } 220 | }) 221 | 222 | // If the previous page set to generate schedules 223 | if ($scope.state.ui.action_generateSchedules) { 224 | $scope.state.ui.action_generateSchedules = false 225 | $scope.generateSchedules() 226 | } 227 | 228 | $scope.resetGenerate = function () { 229 | $scope.state.courses = $scope.state.courses.filter(function (course) { 230 | return !course.fromSelect 231 | }) 232 | 233 | $scope.state.nonCourses.length = 0 234 | $scope.state.noCourses.length = 0 235 | 236 | // Let the lower controllers add a course 237 | $scope.$broadcast('checkForEmptyCourses') 238 | } 239 | }) 240 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Generate/controllers/GenerateNoScheduleCoursesController.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').controller('GenerateNoCourseItemsController', function ($scope) { 2 | $scope.addNoC = function () { 3 | $scope.state.noCourses.push({ 4 | startTime: '', 5 | endTime: '', 6 | days: [] 7 | }) 8 | } 9 | 10 | $scope.removeNoC = function (index: number) { 11 | $scope.state.noCourses.splice(index, 1) 12 | } 13 | 14 | $scope.ensureCorrectEndTime = function (index: number) { 15 | if ($scope.state.noCourses[index].startTime >= $scope.state.noCourses[index].endTime) { 16 | $scope.state.noCourses[index].endTime = $scope.state.noCourses[index].startTime + 60 17 | } 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Generate/controllers/GenerateNonScheduleCoursesController.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').controller('GenerateNonCourseItemsController', function ($scope) { 2 | $scope.addNonC = function () { 3 | $scope.state.nonCourses.push({ 4 | title: '', 5 | startTime: '', 6 | endTime: '', 7 | days: [] 8 | }) 9 | } 10 | 11 | $scope.removeNonC = function (index: number) { 12 | $scope.state.nonCourses.splice(index, 1) 13 | } 14 | 15 | $scope.ensureCorrectEndTime = function (index: number) { 16 | if ($scope.state.nonCourses[index].startTime >= $scope.state.nonCourses[index].endTime) { 17 | $scope.state.nonCourses[index].endTime = $scope.state.nonCourses[index].startTime + 60 18 | } 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Generate/controllers/GenerateScheduleCoursesController.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').controller('GenerateScheduleCoursesController', function ($scope, $http, $q, $timeout) { 2 | // Check if a course needs to be added 3 | const checkEmptyCourses = function () { 4 | if ($scope.state.courses.length === 0 || $scope.courseCart.count.all.coursesFromSelect() === 0) { 5 | $scope.courses_helpers.add() 6 | } 7 | } 8 | checkEmptyCourses() 9 | $scope.$on('checkForEmptyCourses', checkEmptyCourses) 10 | 11 | // Create a way to cancel repeated searches 12 | const canceler = {} 13 | $scope.search = function (course) { 14 | // Check if the course id already has an ajax request and end it. 15 | if (canceler.hasOwnProperty(course.id)) { 16 | canceler[course.id].resolve() 17 | } 18 | 19 | // Create a new request 20 | canceler[course.id] = $q.defer() 21 | 22 | // Set the course to loading status 23 | course.status = 'L' 24 | 25 | // Create the new search request 26 | const searchRequest = $http.post('/generate/getCourseOpts', $.param({ 27 | course: course.search, 28 | term: $scope.state.requestOptions.term, 29 | ignoreFull: $scope.state.requestOptions.ignoreFull 30 | }), { 31 | // Here is where the request gets canceled from above 32 | timeout: canceler[course.id].promise 33 | }).success(function (data: Section[] | ResponseError, status, headers, config) { 34 | // Set loading status to done 35 | course.status = 'D' 36 | 37 | // If there has been no error 38 | if (Array.isArray(data)) { 39 | // set isError and selected to their defaults 40 | for (let c = 0; c < data.length; ++c) { 41 | data[c].isError = false 42 | data[c].selected = true 43 | } 44 | 45 | // Set the data to course's sections 46 | course.sections = data 47 | } else { 48 | // Make a faux-result with isError being true 49 | course.sections = [{ isError: true, error: data }] 50 | } 51 | }) 52 | .error(function (data, status, headers, config) { 53 | // Most likely typed too fast, ignore and set status to done. 54 | course.status = 'D' 55 | }) 56 | } 57 | 58 | // Listen for changes in request options 59 | $scope.$watch('state.requestOptions', function (newRO, oldRO) { 60 | if (angular.equals(newRO, oldRO)) { 61 | return 62 | } 63 | for (let i = 0, l = $scope.state.courses.length; i < l; i++) { 64 | const course = $scope.state.courses[i] 65 | 66 | // Only re-search if the search field was valid anyways 67 | if (course.search.length > 3) { 68 | $scope.search(course) 69 | } 70 | } 71 | }, true) 72 | 73 | // Reset the page size if the new size leaves the current page out of range 74 | $scope.$watch('state.displayOptions.pageSize', function (newPS, oldPS) { 75 | if (newPS === oldPS) { 76 | return 77 | } 78 | if ($scope.state.displayOptions.currentPage + 1 > $scope.numberOfPages()) { 79 | $scope.state.displayOptions.currentPage = $scope.numberOfPages() - 1 80 | } 81 | }) 82 | 83 | // Watch for changes in the course cart 84 | $scope.$watch('state.courses', function (newCourses, oldCourses) { 85 | for (let i = 0, l = newCourses.length; i < l; i++) { 86 | const newCourse = newCourses[i] 87 | 88 | // find the old course that the new one came from 89 | let oldCourse = oldCourses.filter(function (filterCourse) { 90 | return filterCourse.id === newCourse.id 91 | })[0] 92 | 93 | // It's a new course, so mock an old one for comparisons sake 94 | if (typeof oldCourse === 'undefined') { 95 | oldCourse = { 96 | search: '', 97 | sections: [] 98 | } 99 | } 100 | 101 | // Check to see if the search field changed, or was valid 102 | if (newCourse.search !== oldCourse.search && newCourse.search.length > 3) { 103 | // Find the new results! 104 | $scope.search(newCourse) 105 | } else if (newCourse.search !== oldCourse.search) { 106 | // The search field has been changed to be too short, remove sections 107 | newCourse.sections = [] 108 | if (canceler.hasOwnProperty(newCourse.id)) { 109 | canceler[newCourse.id].resolve() 110 | newCourse.status = 'D' 111 | } 112 | } 113 | } 114 | }, true) 115 | }) 116 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Generate/directives/dynamicItemDirective.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').directive('dynamicItem', function ($timeout) { 2 | return { 3 | restrict: 'A', 4 | require: '^dynamicItems', 5 | link: { 6 | pre: function (scope, elm, attrs, dynamicItems) { 7 | scope.$watch('$index', function (newVal) { 8 | scope.index = newVal + 1 9 | if (scope.index === 1) { 10 | $timeout(function () { 11 | elm.addClass('no-repeat-item-animation') 12 | elm.find('input.searchField:first').focus() 13 | }, 0, false) 14 | } 15 | }) 16 | 17 | scope.remove = function () { 18 | if (scope.index === 1 && dynamicItems.items.length === 1) { 19 | elm.removeClass('no-repeat-item-animation') 20 | } 21 | dynamicItems.remove(scope.index) 22 | } 23 | }, 24 | post: function (scope, elm, attrs, dynamicItems) { 25 | let kbdResult 26 | const ident = 'input.searchField' 27 | const input = elm.find(ident) 28 | const doKeystrokeAnalysis = function (e) { 29 | kbdResult = true 30 | if (e.keyCode === 13 && !e.ctrlKey) { 31 | if (dynamicItems.items.length === scope.index) { 32 | dynamicItems.add() 33 | $timeout(function () { 34 | elm.next().find(ident).focus() 35 | }, 0, false) 36 | } else { 37 | elm.next().find(ident).focus() 38 | } 39 | } else if (e.keyCode === 27) { 40 | e.preventDefault() 41 | if (scope.index > 1) { 42 | elm.prev().find(ident).focus() 43 | } else { 44 | const parent = elm.parent() 45 | $timeout(function () { 46 | parent.find(ident + ':first').focus() 47 | }, 0, false) 48 | } 49 | scope.remove() 50 | } else if (e.keyCode === 38 && e.ctrlKey && !e.altKey) { 51 | e.preventDefault() 52 | if (scope.index > 1) { 53 | elm.prev().find(ident).focus() 54 | } 55 | } else if (e.keyCode === 40 && e.ctrlKey && !e.altKey) { 56 | if (scope.index < dynamicItems.items.length) { 57 | elm.next().find(ident).focus() 58 | e.preventDefault() 59 | } 60 | } else if (e.keyCode === 38 && e.ctrlKey && e.altKey) { 61 | scope.showResults = false 62 | kbdResult = false 63 | } else if (e.keyCode === 40 && e.ctrlKey && e.altKey) { 64 | scope.showResults = !scope.showResults 65 | kbdResult = false 66 | } else if (e.ctrlKey && e.altKey && e.keyCode > 48 && e.keyCode < 57) { 67 | if (scope.item.sections.length > 0) { 68 | const index = e.keyCode - 49 69 | const resultElm = scope.item.sections[index] 70 | if (resultElm) { 71 | scope.item.sections[index].selected = !scope.item.sections[index].selected 72 | } 73 | } 74 | } else if (e.ctrlKey && e.altKey && e.keyCode === 65) { 75 | if (scope.item.sections.length > 0) { 76 | let total = 0 77 | for (let i = 0; i < scope.item.sections.length; i++) { 78 | if (scope.item.sections[i].selected) { 79 | total++ 80 | } 81 | } 82 | let target 83 | if (total === scope.item.sections.length) { 84 | target = false 85 | } else { 86 | target = true 87 | } 88 | for (let i = 0; i < scope.item.sections.length; i++) { 89 | scope.item.sections[i].selected = target 90 | } 91 | } 92 | } 93 | } 94 | 95 | input.blur(function (e) { 96 | e.preventDefault() 97 | }) 98 | 99 | input.keydown(function (e) { 100 | scope.$apply(doKeystrokeAnalysis(e)) 101 | return kbdResult 102 | }) 103 | $timeout(function () { 104 | elm.find('input.searchField:first').focus() 105 | }, 0, false) 106 | } 107 | } 108 | } 109 | }) 110 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Generate/directives/dynamicItemsDirective.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').directive('dynamicItems', function ($compile, $timeout, globalKbdShortcuts) { 2 | return { 3 | restrict: 'A', 4 | scope: { 5 | dynamicItems: '=', 6 | useClass: '@', 7 | helpers: '=', 8 | colors: '=' 9 | }, 10 | controller: function ($scope) { 11 | this.items = $scope.dynamicItems 12 | this.add = $scope.helpers.add 13 | this.remove = $scope.helpers.remove 14 | }, 15 | compile: function (telm, tattrs) { 16 | return { 17 | pre: function (scope, elm, attrs) { 18 | scope.$parent.$on('addedCourse', function () { 19 | $timeout(function () { 20 | elm.find('input.searchField:last').focus() 21 | }, 0, false) 22 | }) 23 | elm.append($compile('
')(scope)) 24 | }, 25 | post: function (scope, elm, attrs) { 26 | globalKbdShortcuts.bindSelectCourses(function () { 27 | if (elm.find('input.searchField:focus').length === 0) { 28 | $('html, body').animate({ 29 | scrollTop: 0 30 | }, 500, null, function () { 31 | elm.find('input.searchField:first').focus() 32 | }) 33 | } 34 | }) 35 | } 36 | } 37 | } 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Generate/directives/scheduleCourseDirective.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').directive('scheduleCourse', function () { 2 | return { 3 | restrict: 'C', 4 | templateUrl: '/<%=modulePath%>Generate/templates/courseselect.min.html' 5 | } 6 | }) 7 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Generate/templates/courseselect.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 21 |
22 |
23 |
{{item.sections[0].error.msg}}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
    31 |
  • 32 |
    33 |
    34 |

    {{$index + 1}}. {{section.courseNum}}

    35 | {{section.title}} 36 |

    37 | 38 |

    39 |
    40 |
    {{time.days}} {{time.start | formatTime}}-{{time.end | formatTime}}
    41 |
    42 |
    43 |
    44 |
    45 |
    46 | 47 |
    48 |
    49 |
    50 |
    {{section.curenroll}}/{{section.maxenroll}}
    51 |
    52 |
    53 |
    54 |
  • 55 |
56 |
57 |
58 |
59 |
60 | 61 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Index/templates/index.html: -------------------------------------------------------------------------------- 1 |
2 | 22 |
-------------------------------------------------------------------------------- /assets/src/modules/sm/Schedule/controllers/ScheduleController.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').controller('ScheduleController', function ($scope, parsedSchedule) { 2 | if (!parsedSchedule.error) { 3 | if (parsedSchedule.hasOwnProperty('courses')) { 4 | $scope.schedule = parsedSchedule.courses 5 | } else if (parsedSchedule.hasOwnProperty('schedule')) { 6 | $scope.schedule = parsedSchedule.schedule 7 | } else { 8 | $scope.schedule = [] 9 | } 10 | } else { 11 | $scope.schedule = [] 12 | } 13 | 14 | if ($scope.schedule.length > 0) { 15 | $scope.overrideDrawOptions = {} 16 | 17 | // Set the correct draw options 18 | for (const key in $scope.state.drawOptions) { 19 | let overrideValue = parsedSchedule[key] 20 | if (typeof overrideValue === 'undefined' || overrideValue === null) { 21 | overrideValue = $scope.state.drawOptions[key] 22 | } 23 | $scope.overrideDrawOptions[key] = overrideValue 24 | } 25 | 26 | // Set image property 27 | if (parsedSchedule.hasOwnProperty('image')) { 28 | $scope.imageSupport = parsedSchedule.image 29 | } else { 30 | $scope.imageSupport = true 31 | } 32 | 33 | // Set the correct term, 34 | $scope.state.ui.temp_savedScheduleTerm = parsedSchedule.term 35 | } 36 | 37 | $scope.$on('$destroy', function () { 38 | $scope.imageSupport = true 39 | $scope.overrideDrawOptions = null 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Schedule/controllers/SchedulePrintController.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').controller('SchedulePrintController', function ($scope, $location, localStorage) { 2 | if ($scope.schedule) { 3 | const pTerm = '' + $scope.state.requestOptions.term 4 | 5 | const year = parseInt(pTerm.substring(0, 4)) 6 | let term = pTerm.substring(4) 7 | if (year >= 2013) { 8 | switch (term) { 9 | case '1': term = 'Fall'; break 10 | case '3': term = 'Winter Intersession'; break 11 | case '5': term = 'Spring'; break 12 | case '8': term = 'Summer'; break 13 | default: term = 'Unknown' 14 | } 15 | } else { 16 | switch (term) { 17 | case '1': term = 'Fall'; break 18 | case '2': term = 'Winter'; break 19 | case '3': term = 'Spring'; break 20 | case '4': term = 'Summer'; break 21 | default: term = 'Unknown' 22 | } 23 | } 24 | 25 | $scope.heading = 'My ' + year + '-' + (year + 1) + ' ' + term + ' Schedule' 26 | } 27 | 28 | localStorage.setItem('reloadSchedule', null) 29 | 30 | $scope.printFn = window.print.bind(window) 31 | 32 | $scope.globalUI.layoutClass = 'print' 33 | }) 34 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Schedule/controllers/ScheduleViewController.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').controller('ScheduleViewController', function ($scope, $location, $stateParams) { 2 | const id = $stateParams.id 3 | $scope.saveInfo = { 4 | url: $location.absUrl(), 5 | id: id 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Schedule/directives/scheduleActionsDirective.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Several endpoint abstractions for the schedules 3 | */ 4 | angular.module('sm').directive('scheduleActions', function ($http, $q, shareServiceInfo, openPopup, localStorage, $state, $timeout) { 5 | const serializer = new XMLSerializer() 6 | 7 | function scheduleActions (scope, elm) { 8 | const getSavedInfo = function () { 9 | // See if we already have saved info 10 | if (scope.saveInfo) { 11 | const defferred = $q.defer() 12 | defferred.resolve(scope.saveInfo) 13 | return defferred.promise 14 | } 15 | // If not create it 16 | const schedule = angular.copy(scope.schedule) 17 | scope.status = 'L' 18 | 19 | // Create the request params as all strings with correct keys 20 | const params = { 21 | data: JSON.stringify({ 22 | startday: '' + scope.state.drawOptions.startDay, 23 | endday: '' + scope.state.drawOptions.endDay, 24 | starttime: '' + scope.state.drawOptions.startTime, 25 | endtime: '' + scope.state.drawOptions.endTime, 26 | building: '' + scope.state.drawOptions.bldgStyle, 27 | term: '' + scope.state.requestOptions.term, 28 | schedule: schedule 29 | }), 30 | svg: serializer.serializeToString(elm.find('svg').get(0)) 31 | } 32 | 33 | // Post the schedule and return a promise 34 | return $http.post('/schedule/new', $.param(params), { 35 | requestType: 'json' 36 | }) 37 | .then(function (request) { 38 | if (request.status === 200 && typeof request.data.error === 'undefined') { 39 | // save the saveInfo and return it 40 | scope.saveInfo = request.data 41 | scope.status = 'D' 42 | return request.data 43 | } else { 44 | return $q.reject('Save Error:' + request.data.msg) 45 | } 46 | }) 47 | } 48 | 49 | scope.scheduleActions = { 50 | 51 | save: function (saveType) { 52 | if (saveType === 'create') { 53 | ga('send', 'event', 'schedule', 'save') 54 | window.DD_RUM && 55 | window.DD_RUM.addAction('Schedule', { 56 | type: 'Save' 57 | }) 58 | getSavedInfo().then(function (data) { 59 | scope.notification = 'This schedule can be accessed at ' + 60 | '' + 61 | data.url + '
This schedule will be removed' + 62 | ' after 3 months of inactivity' 63 | }, function (error) { 64 | console.log(error) 65 | scope.notification = error 66 | }) 67 | } else { 68 | ga('send', 'event', 'schedule', 'fork') 69 | window.DD_RUM && 70 | window.DD_RUM.addAction('Schedule', { 71 | type: 'Fork' 72 | }) 73 | 74 | localStorage.setItem('forkSchedule', scope.schedule) 75 | $state.go('generate') 76 | } 77 | }, 78 | 79 | shareToService: function ($event, serviceName, newWindow) { 80 | ga('send', 'event', 'schedule', 'share', serviceName) 81 | window.DD_RUM && 82 | window.DD_RUM.addAction('Schedule', { 83 | type: 'Share', 84 | serviceName: serviceName 85 | }) 86 | $event.preventDefault() 87 | scope.status = 'L' 88 | if (serviceName && serviceName in shareServiceInfo) { 89 | const service = shareServiceInfo[serviceName] 90 | 91 | // Create a popup in click context to workaround blockers 92 | const popup = openPopup(newWindow) 93 | 94 | getSavedInfo().then(function (data) { 95 | scope.status = 'D' 96 | popup.location = service(data.url) 97 | }) 98 | } 99 | }, 100 | 101 | shareToEmail: function ($event) { 102 | ga('send', 'event', 'schedule', 'share', 'email') 103 | window.DD_RUM && 104 | window.DD_RUM.addAction('Schedule', { 105 | type: 'Share', 106 | subtype: 'Email' 107 | }) 108 | $event.preventDefault() 109 | 110 | getSavedInfo().then(function (data) { 111 | const body = 'Check out my schedule at: ' + data.url 112 | 113 | // Open a mailto link 114 | window.location.href = 'mailto:?body=' + 115 | encodeURIComponent(body) 116 | }) 117 | }, 118 | 119 | shareToDirectLink: function ($event) { 120 | ga('send', 'event', 'schedule', 'share', 'link') 121 | window.DD_RUM && 122 | window.DD_RUM.addAction('Schedule', { 123 | type: 'Share', 124 | subtype: 'Link' 125 | }) 126 | $event.preventDefault() 127 | 128 | scope.scheduleActions.save('create') 129 | }, 130 | 131 | downloadiCal: function ($event) { 132 | ga('send', 'event', 'schedule', 'download', 'iCal') 133 | window.DD_RUM && 134 | window.DD_RUM.addAction('Schedule', { 135 | type: 'Download', 136 | subtype: 'iCal' 137 | }) 138 | $event.preventDefault() 139 | 140 | getSavedInfo().then(function (data) { 141 | window.location.href = data.url + '/ical' 142 | }) 143 | }, 144 | 145 | downloadImage: function ($event) { 146 | ga('send', 'event', 'schedule', 'download', 'image') 147 | window.DD_RUM && 148 | window.DD_RUM.addAction('Schedule', { 149 | type: 'Download', 150 | subtype: 'Image' 151 | }) 152 | $event.preventDefault() 153 | 154 | const popup = openPopup(true) 155 | 156 | getSavedInfo().then(function (data) { 157 | popup.location = ('http://' + window.location.hostname + 158 | '/img/schedules/' + parseInt(data.id, 16) + '.png') 159 | }) 160 | }, 161 | 162 | print: function () { 163 | ga('send', 'event', 'schedule', 'print') 164 | window.DD_RUM && 165 | window.DD_RUM.addAction('Schedule', { 166 | type: 'Print' 167 | }) 168 | 169 | const reloadSchedule = angular.copy(scope.state.drawOptions) 170 | reloadSchedule.term = scope.state.requestOptions.term 171 | reloadSchedule.courses = scope.schedule 172 | 173 | const popup = openPopup(920, 800) 174 | 175 | popup.localStorage.setItem('reloadSchedule', angular.toJson(reloadSchedule)) 176 | popup.document.title = 'My Schedule' 177 | popup.location = 'http://' + window.location.hostname + '/schedule/render/print' 178 | }, 179 | 180 | hide: function () { 181 | ga('send', 'event', 'schedule', 'hide') 182 | window.DD_RUM && 183 | window.DD_RUM.addAction('Schedule', { 184 | type: 'Hide' 185 | }) 186 | const appstate = scope.$parent.$parent.state 187 | const pageStartIndex = appstate.displayOptions.currentPage * appstate.displayOptions.pageSize 188 | 189 | appstate.schedules.splice(pageStartIndex + scope.$index, 1) 190 | } 191 | } 192 | }; 193 | 194 | return { 195 | 196 | /** 197 | * Save a schedule, given the respective parameters 198 | */ 199 | link: { 200 | pre: scheduleActions 201 | } 202 | } 203 | }) 204 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Schedule/directives/svgTextLineDirective.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').directive('svgTextLine', function () { 2 | return { 3 | link: function (scope, elm, attrs) { 4 | let text = attrs.svgTextLine 5 | const adjust = (scope.print) ? 1 : 0 6 | const cutoff = 25 + (adjust * -7) 7 | 8 | if (scope.grid.days.length > 3) { 9 | if (text.length > 14) { 10 | const element = elm.get(0) 11 | element.setAttribute('textLength', (parseFloat(scope.grid.opts.daysWidth) - 1) + '%') 12 | element.setAttribute('lengthAdjust', 'spacingAndGlyphs') 13 | } 14 | if (text.length > cutoff) { 15 | text = text.slice(0, cutoff - 3) + '...' 16 | } 17 | } 18 | elm.text(text) 19 | } 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Schedule/providers/reloadScheduleFactory.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').factory('reloadSchedule', function ($http, $q, localStorage) { 2 | /** 3 | * Set the correct drawOptions and term as well as a global schedule var 4 | * for displaying any single schedule alone 5 | */ 6 | 7 | const getEmptySchedule = function () { 8 | return { 9 | schedule: [] 10 | } 11 | } 12 | 13 | return function ($stateParams) { 14 | const deferred = $q.defer() 15 | 16 | // Check if 17 | if ($stateParams.hasOwnProperty('id') && $stateParams.id !== 'render') { 18 | // We need to get the schedule 19 | $http.get('/schedule/' + $stateParams.id) 20 | .then(function (response) { 21 | if (response.status === 200 && !response.data.error) { 22 | deferred.resolve(response.data) 23 | } else { 24 | deferred.resolve(response.data) 25 | } 26 | }, function () { 27 | deferred.resolve(getEmptySchedule()) 28 | }) 29 | } else if (localStorage.hasKey('reloadSchedule')) { 30 | // Get the schedule from sessions storage 31 | const reloadSchedule = localStorage.getItem('reloadSchedule') 32 | // If it's actually there 33 | if (reloadSchedule != null) { 34 | deferred.resolve(reloadSchedule) 35 | localStorage.setItem('reloadSchedule', null) 36 | } else { 37 | deferred.resolve(getEmptySchedule()) 38 | } 39 | } else { 40 | deferred.resolve(getEmptySchedule()) 41 | } 42 | 43 | return deferred.promise 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Schedule/templates/schedule.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |
7 | The requested schedule was not found. 8 |
9 |
-------------------------------------------------------------------------------- /assets/src/modules/sm/Schedule/templates/schedule.print.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

6 | Print Options For best results, print landscape, turn off headings/footers, and set the margins to .25" 7 |

8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 | 16 |
17 |
18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 |
28 |
29 | 30 |
31 | 32 |
33 |
34 |
35 |
36 | 39 |
40 |
41 |
42 |
43 |
44 | 45 |
-------------------------------------------------------------------------------- /assets/src/modules/sm/Schedule/templates/schedule.view.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
-------------------------------------------------------------------------------- /assets/src/modules/sm/Search/controllers/SearchController.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The controller holding all the logic for the search page 3 | */ 4 | 5 | angular.module('sm').controller('SearchController', function ($scope, $http, entityDataRequest, globalKbdShortcuts) { 6 | const defaultOptions = { 7 | college: { id: 'any', code: 'any', number: null, title: 'Any College' }, 8 | department: { id: 'any', code: 'any', number: null, title: 'Any Department' } 9 | } 10 | 11 | $scope.searchResults = [] 12 | 13 | $scope.search = { 14 | params: {}, 15 | options: { 16 | colleges: [defaultOptions.college], 17 | departments: [defaultOptions.department] 18 | } 19 | }; 20 | 21 | // Init the search parmeters without changing their object identity 22 | ($scope.initSearch = function () { 23 | const sP = $scope.search.params 24 | sP.term = $scope.state.requestOptions.term 25 | sP.college = 'any' 26 | sP.department = 'any' 27 | sP.level = 'any' 28 | sP.credits = '' 29 | sP.professor = '' 30 | sP.daysAny = true 31 | sP.days = [] 32 | sP.timesAny = true 33 | sP.times = { 34 | morn: false, 35 | aftn: false, 36 | even: false 37 | } 38 | sP.online = true 39 | sP.honors = true 40 | sP.offCampus = true 41 | 42 | sP.title = '' 43 | sP.description = '' 44 | })() 45 | 46 | const reloadSchoolsForTerm = function (newTerm, oldTerm) { 47 | if (newTerm === oldTerm) return 48 | 49 | // Set the new term in our params 50 | $scope.search.params.term = newTerm 51 | 52 | // Reset our selected options 53 | $scope.search.params.college = 'any' 54 | $scope.search.params.department = 'any' 55 | 56 | // Get a list of schools for the term 57 | entityDataRequest.getSchoolsForTerm({ term: newTerm }) 58 | .success(function (data, status) { 59 | if (status === 200 && typeof data.error === 'undefined') { 60 | // Push the default to the top and set it as the option list 61 | data.unshift(defaultOptions.college) 62 | $scope.search.options.colleges = data 63 | } else if (data.error) { 64 | // TODO: Better error checking 65 | alert(data.msg) 66 | } 67 | }) 68 | } 69 | reloadSchoolsForTerm($scope.state.requestOptions.term, '') 70 | 71 | // Listen for term changes 72 | $scope.$watch('state.requestOptions.term', reloadSchoolsForTerm) 73 | 74 | // Reload the departments when a college is selected 75 | $scope.$watch('search.params.college', function (newCollege) { 76 | if (newCollege !== '' && newCollege !== 'any') { 77 | // Reset selected department 78 | $scope.search.params.department = 'any' 79 | 80 | // Get a list of departments 81 | entityDataRequest.getDepartmentsForSchool({ 82 | term: $scope.search.params.term, 83 | param: newCollege 84 | }).success(function (data, status) { 85 | if (status === 200 && typeof data.error === 'undefined') { 86 | // Push the default to the top and set it as the option list 87 | data.departments.unshift(defaultOptions.department) 88 | $scope.search.options.departments = data.departments 89 | } else if (data.error) { 90 | // TODO: Better error checking 91 | alert(data.msg) 92 | } 93 | }) 94 | } else if ($scope.search.options.departments.length > 1) { 95 | // Reset if there were more than one options already out 96 | $scope.search.options.departments = [defaultOptions.department] 97 | } 98 | }) 99 | 100 | // 'D'one loading 101 | $scope.searchStatus = 'D' 102 | 103 | $scope.findMatches = function () { 104 | // Only search if a current search is not in progress 105 | if ($scope.searchStatus === 'L') return 106 | 107 | // 'L'oading 108 | $scope.searchStatus = 'L' 109 | 110 | const params = angular.copy($scope.search.params) 111 | 112 | // Remove uneeded data 113 | if (params.timesAny === true) { 114 | delete params.times 115 | } else { 116 | const times = [] 117 | for (const time in params.times) { 118 | if (params.times[time] === true) { 119 | times.push(time) 120 | } 121 | } 122 | 123 | if (times.length === 0) { 124 | delete params.times 125 | } else { 126 | params.times = times 127 | } 128 | } 129 | delete params.timesAny 130 | 131 | if (params.daysAny === true || params.days.length === 0) { 132 | delete params.days 133 | } 134 | delete params.daysAny 135 | 136 | $http.post('/search/find', $.param(params)) 137 | .success(function (data, status) { 138 | // 'D'one loading 139 | $scope.searchStatus = 'D' 140 | 141 | ga('send', 'event', 'search', 'find') 142 | window.DD_RUM && 143 | window.DD_RUM.addAction('Search', { 144 | type: 'Find', 145 | data: data 146 | }) 147 | 148 | if (status === 200 && typeof data.error === 'undefined') { 149 | // Set the results 150 | $scope.searchResults = data 151 | 152 | // Reset to the first page and scroll 153 | $scope.searchPagination.currentPage = 0 154 | $scope.scrollToResults() 155 | 156 | // Remove any errors if they were present 157 | if ($scope.resultError) { 158 | $scope.resultError = null 159 | } 160 | } else if (data.error) { 161 | $scope.resultError = data.msg 162 | 163 | // Clear result 164 | $scope.searchResults = [] 165 | } 166 | }) 167 | } 168 | 169 | $scope.searchPagination = { 170 | pageSize: 10, 171 | currentPage: 0 172 | } 173 | 174 | $scope.numberOfPages = function () { 175 | return Math.ceil($scope.searchResults.length / $scope.searchPagination.pageSize) 176 | } 177 | 178 | $scope.$watch('searchPagination.pageSize', function (newSize, oldSize) { 179 | if (newSize !== oldSize) { 180 | const numPages = $scope.numberOfPages() 181 | if ($scope.searchPagination.currentPage > numPages) { 182 | $scope.searchPagination.currentPage = numPages - 1 183 | } 184 | } 185 | }) 186 | 187 | $scope.scrollToResults = function () { 188 | // Again, I know this is bad, but I'm lazy 189 | setTimeout(function () { 190 | $('input:focus').blur() 191 | $('html, body').animate({ 192 | scrollTop: $('#search_results').offset().top - 65 193 | }, 500) 194 | }, 100) 195 | } 196 | 197 | globalKbdShortcuts.bindEnter($scope.findMatches) 198 | globalKbdShortcuts.bindPagination(function () { 199 | if (this.keyCode === 39 && $scope.searchPagination.currentPage + 1 < $scope.numberOfPages()) { 200 | $scope.searchPagination.currentPage++ 201 | $scope.scrollToResults() 202 | } else if (this.keyCode === 37 && $scope.searchPagination.currentPage - 1 >= 0) { 203 | $scope.searchPagination.currentPage-- 204 | $scope.scrollToResults() 205 | } 206 | }) 207 | }) 208 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Status/controllers/StatusController.ts: -------------------------------------------------------------------------------- 1 | angular.module('sm').controller('StatusController', function ($scope, $http) { 2 | $scope.logs = [] 3 | 4 | $http.get('/status') 5 | .success(function (data, status, headers, config) { 6 | if (status === 200 && !data.error) { 7 | $scope.logs = data 8 | } else { 9 | // TODO: Better error checking 10 | alert($scope.error) 11 | } 12 | }) 13 | 14 | $scope.timeConvert = function (UnixTimestamp) { 15 | const a = new Date(+UnixTimestamp * 1000) 16 | const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 17 | const year = a.getFullYear() 18 | const month = months[a.getMonth()] 19 | const date = a.getDate() 20 | const hour = a.getHours() 21 | let min: number | string = a.getMinutes() 22 | let sec: number | string = a.getSeconds() 23 | if (sec <= 10) sec = '0' + sec 24 | if (min <= 10) min = '0' + min 25 | const time = month + ' ' + date + ' ' + year + ' ' + hour + ':' + min + ':' + sec 26 | return time 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /assets/src/modules/sm/Status/templates/status.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Last 20 Data Scrapes
Last run: {{logs.length > 0?timeConvert(logs[0].timeStarted):'Never'}}

5 |
6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
Scrape StartedScrape FinishedTime ElapsedCourses AddedCourses UpdatedSections AddedSections UpdatedFailures
No Logs Exist
35 |
36 |
37 |
38 |
-------------------------------------------------------------------------------- /assets/src/modules/sm/app.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Initialize the main sm module 3 | */ 4 | angular.module('sm', ['ngAnimate', 'ngSanitize', 'ui.router']) 5 | /** 6 | * Core Config code 7 | */ 8 | .config(function ($stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) { 9 | $httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8' 10 | 11 | $locationProvider.html5Mode({ 12 | enabled: true, 13 | requireBase: false 14 | }) 15 | 16 | $urlRouterProvider.otherwise('/404') 17 | 18 | const tplBase = '/<%=modulePath%>' 19 | 20 | const tplPath = function (submodule, name) { 21 | return tplBase + submodule + '/templates/' + name + '.min.html' 22 | } 23 | 24 | $stateProvider 25 | .state('index', { 26 | url: '/', 27 | templateUrl: tplPath('Index', 'index') 28 | }) 29 | .state('404', { 30 | url: '/404', 31 | templateUrl: tplPath('App', '404') 32 | }) 33 | .state('generate', { 34 | url: '/generate', 35 | templateUrl: tplPath('Generate', 'generate'), 36 | controller: 'GenerateController' 37 | }) 38 | .state('browse', { 39 | url: '/browse', 40 | templateUrl: tplPath('Browse', 'browse'), 41 | controller: 'BrowseController' 42 | }) 43 | .state('search', { 44 | url: '/search', 45 | templateUrl: tplPath('Search', 'search'), 46 | controller: 'SearchController' 47 | }) 48 | .state('help', { 49 | url: '/help', 50 | templateUrl: tplPath('App', 'help') 51 | }) 52 | .state('status', { 53 | url: '/status', 54 | templateUrl: tplPath('Status', 'status'), 55 | controller: 'StatusController' 56 | }).state('schedule', { 57 | url: '/schedule/:id', 58 | templateUrl: tplPath('Schedule', 'schedule'), 59 | resolve: { 60 | parsedSchedule: ['$stateParams', 'reloadSchedule', function ($stateParams, reloadSchedule) { 61 | return reloadSchedule($stateParams) 62 | }] 63 | }, 64 | abstract: true, 65 | controller: 'ScheduleController' 66 | }).state('schedule.view', { 67 | url: '', 68 | templateUrl: tplPath('Schedule', 'schedule.view'), 69 | controller: 'ScheduleViewController' 70 | }).state('schedule.print', { 71 | url: '/print', 72 | templateUrl: tplPath('Schedule', 'schedule.print'), 73 | controller: 'SchedulePrintController' 74 | }) 75 | }) 76 | /** 77 | * Core run-time code 78 | */ 79 | .run(function ($rootScope, $window) { 80 | $rootScope.$on('$stateChangeSuccess', function (evt) { 81 | ga('send', 'pageview') 82 | window.DD_RUM && 83 | window.DD_RUM.addAction('Pageview', { 84 | type: 'On Load' 85 | }) 86 | $($window).scrollTop(0) 87 | }) 88 | 89 | // IE 10 MOBILE FIX 90 | if (navigator.userAgent.match(/IEMobile\/10\.0/)) { 91 | const msViewportStyle = document.createElement('style') 92 | msViewportStyle.appendChild( 93 | document.createTextNode( 94 | '@-ms-viewport{width:auto!important}' 95 | ) 96 | ) 97 | document.getElementsByTagName('head')[0].appendChild(msViewportStyle) 98 | } 99 | }) 100 | -------------------------------------------------------------------------------- /assets/src/modules/sm/global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable camelcase */ 3 | declare let angular: any 4 | 5 | declare interface Department { 6 | code: string 7 | number: null 8 | } 9 | 10 | declare interface Bldg { 11 | code: string; 12 | number: string; 13 | } 14 | 15 | declare interface Time { 16 | bldg: Bldg; 17 | room: string; 18 | day: string; 19 | start: string; 20 | end: string; 21 | off_campus: boolean; 22 | } 23 | declare interface Section { 24 | title: string; 25 | instructor: string; 26 | curenroll: string; 27 | maxenroll: string; 28 | courseNum: string; 29 | courseParentNum: string; 30 | courseId: string; 31 | id: string; 32 | online: boolean; 33 | credits: string; 34 | times: Time[]; 35 | isError?: boolean 36 | selected?: boolean 37 | } 38 | declare interface Course { 39 | id: string 40 | sections: Section[] 41 | title: string 42 | courseNum: string 43 | course: string 44 | department: Department 45 | times: Time[] 46 | fromSelect: boolean 47 | selected: boolean 48 | description: string 49 | search 50 | } 51 | 52 | declare interface ResponseError { 53 | error 54 | } 55 | 56 | declare interface Window { DD_RUM } 57 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "computersciencehouse/schedulemaker", 3 | "description": "A course database lookup tool and schedule building web application for use at Rochester Institute of Technology.", 4 | "type": "project", 5 | "authors": [ 6 | { 7 | "name": "Devin Matte", 8 | "email": "matted@csh.rit.edu" 9 | }, 10 | { 11 | "name": "Ben Grawi", 12 | "email": "bgrawi@csh.rit.edu" 13 | }, 14 | { 15 | "name": "Ben Russell", 16 | "email": "benrr101@csh.rit.edu" 17 | }, 18 | { 19 | "name": "John Resig", 20 | "email": "phytar@csh.rit.edu" 21 | } 22 | ], 23 | "require": { 24 | "php": ">=7.3", 25 | "aws/aws-sdk-php": "^3.69", 26 | "ext-imagick": "*", 27 | "ext-json": "*" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ComputerScienceHouse/schedulemaker/52f58d92c9dc3ee245a48fe9a78319c081a23b6a/favicon.ico -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable standard/no-callback-literal */ 2 | 3 | // Get the version info 4 | const pkg = require('./package.json') 5 | if (!pkg.version) { 6 | console.error('No version information in package.json.') 7 | proccess.exit() 8 | } 9 | 10 | const assetsRoot = { 11 | src: 'assets/src/', 12 | dist: 'assets/dist/', 13 | dest: 'assets/prod/' 14 | } 15 | 16 | // Set up core routes 17 | const modulesRoot = { 18 | src: assetsRoot.src + 'modules/', 19 | dist: assetsRoot.dist + 'modules/', 20 | dest: assetsRoot.dest + pkg.version + '/modules/' 21 | } 22 | 23 | const assetModuleList = { 24 | sm: ['App', 'Schedule', 'Generate', 'Browse', 'Search', 'Help', 'Index'] 25 | } 26 | 27 | const assetTypes = { 28 | scripts: { 29 | paths: [ 30 | '', 31 | 'providers/', 32 | 'directives/', 33 | 'controllers/' 34 | ], 35 | selector: '*.js' 36 | }, 37 | styles: { 38 | paths: [ 39 | '', 40 | 'styles/' 41 | ], 42 | selector: '*.css' 43 | }, 44 | templates: { 45 | paths: [ 46 | '', 47 | 'templates/' 48 | ], 49 | selector: '*.html' 50 | } 51 | } 52 | 53 | const paths = {} 54 | const distPaths = {} 55 | 56 | const getPaths = (rootPath, pathsDict) => { 57 | for (const moduleName in assetModuleList) { 58 | subModuleList = assetModuleList[moduleName] 59 | 60 | pathsDict[moduleName] = {} 61 | for (const assetType in assetTypes) { 62 | const assetOpts = assetTypes[assetType] 63 | 64 | pathsDict[moduleName][assetType] = [] 65 | 66 | assetOpts.paths.forEach(function (assetPath) { 67 | pathsDict[moduleName][assetType].push(rootPath + moduleName + '/**/' + assetPath + assetOpts.selector) 68 | }) 69 | } 70 | } 71 | } 72 | 73 | const doFor = function (assetType, cb) { 74 | const streamResults = [] 75 | for (const moduleName in assetModuleList) { 76 | streamResults.push(cb({ 77 | src: paths[moduleName][assetType], 78 | dist: distPaths[moduleName][assetType], 79 | dest: modulesRoot.dest + moduleName + '/' 80 | })) 81 | } 82 | return streamResults 83 | } 84 | 85 | getPaths(modulesRoot.src, paths) 86 | 87 | // Import required plugins 88 | const gulp = require('gulp') 89 | const htmlmin = require('gulp-htmlmin') 90 | const ngAnnotate = require('gulp-ng-annotate') 91 | const uglify = require('gulp-uglify') 92 | const concat = require('gulp-concat') 93 | const rename = require('gulp-rename') 94 | const sourcemaps = require('gulp-sourcemaps') 95 | const replace = require('gulp-replace') 96 | const es = require('event-stream') 97 | const minifyCSS = require('gulp-minify-css') 98 | const template = require('gulp-template') 99 | const ts = require('gulp-typescript') 100 | const del = require('del') 101 | const vinylPaths = require('vinyl-paths') 102 | 103 | const tsProject = ts.createProject('tsconfig.json') 104 | 105 | // Define Tasks 106 | gulp.task('templates', function (done) { 107 | const mapped = doFor('templates', function (templatePaths) { 108 | return gulp.src(templatePaths.src) 109 | .pipe(htmlmin({ 110 | collapseWhitespace: true, 111 | caseSensitive: true, 112 | keepClosingSlash: true 113 | })) 114 | .pipe(rename({ suffix: '.min' })) 115 | .pipe(gulp.dest(templatePaths.dest)) 116 | }) 117 | 118 | done() 119 | return es.concat.apply(null, mapped) 120 | }) 121 | 122 | gulp.task('compile', function () { 123 | const tsOut = tsProject.src() 124 | .pipe(tsProject()) 125 | .js.pipe(gulp.dest(modulesRoot.dist + 'sm/')) 126 | getPaths(modulesRoot.dist, distPaths) 127 | return tsOut 128 | }) 129 | 130 | gulp.task('scripts', function (done) { 131 | const mapped = doFor('scripts', function (scriptPaths) { 132 | return gulp.src(scriptPaths.dist) 133 | .pipe(template({ modulePath: scriptPaths.dest })) 134 | .pipe(ngAnnotate()) 135 | .pipe(concat('dist.js')) 136 | .pipe(gulp.dest(scriptPaths.dest)) 137 | .pipe(sourcemaps.init()) 138 | .pipe(rename({ suffix: '.min' })) 139 | .pipe(uglify({ outSourceMap: 'dist.min.js' })) 140 | .pipe(sourcemaps.write({ inline: false, includeContent: false })) 141 | // HACK UNTIL GRUNT-UGLIFY HANDLES SOURCEMAPS CORRECTLY 142 | .pipe(replace('{"version":3,"file":"dist.min.js","sources":["dist.min.js"]', '{"version":3,"file":"dist.min.js","sources":["dist.js"]')) 143 | .pipe(gulp.dest(scriptPaths.dest)) 144 | }) 145 | 146 | done() 147 | return es.concat.apply(null, mapped) 148 | }) 149 | 150 | gulp.task('styles', function (done) { 151 | const mapped = doFor('styles', function (stylePaths) { 152 | return gulp.src(stylePaths.src) 153 | .pipe(concat('dist.css')) 154 | .pipe(gulp.dest(stylePaths.dest)) 155 | .pipe(minifyCSS({ advanced: false, processImport: true, keepSpecialComments: 0 })) 156 | .pipe(rename({ suffix: '.min' })) 157 | .pipe(gulp.dest(stylePaths.dest)) 158 | }) 159 | 160 | done() 161 | return es.concat.apply(null, mapped) 162 | }) 163 | 164 | gulp.task('watch', function () { 165 | doFor('templates', function (templatePaths) { 166 | gulp.watch(templatePaths.src, gulp.series('templates')).on('error', function () { 167 | }) 168 | }) 169 | doFor('scripts', function (scriptPaths) { 170 | gulp.watch(scriptPaths.dist, gulp.series('scripts')).on('error', function () { 171 | }) 172 | }) 173 | doFor('styles', function (stylesPaths) { 174 | gulp.watch(stylesPaths.src, gulp.series('styles')).on('error', function () { 175 | }) 176 | }) 177 | }) 178 | 179 | gulp.task('clean', function () { 180 | return gulp.src(modulesRoot.dest, { read: false, allowEmpty: true }) 181 | .pipe(vinylPaths(del)) 182 | }) 183 | 184 | gulp.task('cleanAll', function () { 185 | return gulp.src([assetsRoot.dest + '/*', '!' + assetsRoot.dest + '.gitkeep'], { read: false, allowEmpty: true }) 186 | .pipe(vinylPaths(del)) 187 | }) 188 | 189 | gulp.task('build', gulp.series('clean', 'compile', gulp.parallel('scripts', 'templates', 'styles'))) 190 | 191 | gulp.task('default', gulp.series('build')) 192 | -------------------------------------------------------------------------------- /img/csh_logo_square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | ]> 6 | 10 | 11 | 12 | 14 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /img/csh_og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ComputerScienceHouse/schedulemaker/52f58d92c9dc3ee245a48fe9a78319c081a23b6a/img/csh_og.png -------------------------------------------------------------------------------- /img/csh_print.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ComputerScienceHouse/schedulemaker/52f58d92c9dc3ee245a48fe9a78319c081a23b6a/img/csh_print.png -------------------------------------------------------------------------------- /inc/ajaxError.php: -------------------------------------------------------------------------------- 1 | "php", 15 | "msg" => "A internal server error occurred", 16 | "guru" => $errstr, 17 | "num" => $errno, 18 | "file" => "{$errfile}:{$errline}" 19 | ])); 20 | } 21 | 22 | set_error_handler("errorHandler"); 23 | -------------------------------------------------------------------------------- /inc/config.env.php: -------------------------------------------------------------------------------- 1 | $v) { 58 | unset($process[$key][$k]); 59 | if (is_array($v)) { 60 | $process[$key][stripslashes($k)] = $v; 61 | $process[] = &$process[$key][stripslashes($k)]; 62 | } else { 63 | $process[$key][stripslashes($k)] = stripslashes($v); 64 | } 65 | } 66 | } 67 | unset($process); 68 | } 69 | //////////////////////////////////////////////////////////////////////////// 70 | // CALCULATIONS 71 | 72 | // Calculate the current quarter 73 | switch(date('n')) { 74 | case 2: 75 | case 3: 76 | $CURRENT_QUARTER = date("Y")-1 . '3'; // Point them to the spring 77 | break; 78 | case 4: 79 | case 5: 80 | case 6: 81 | case 7: 82 | case 8: 83 | case 9: 84 | $CURRENT_QUARTER = date("Y") . '1'; // Point them to the fall 85 | break; 86 | case 10: 87 | case 11: 88 | case 12: 89 | case 1: 90 | $CURRENT_QUARTER = date("Y") . '2'; // Point them to the summer 91 | break; 92 | } 93 | 94 | -------------------------------------------------------------------------------- /inc/config.example.php: -------------------------------------------------------------------------------- 1 | "Monday", 26 | "Tue" => "Tuesday", 27 | "Wed" => "Wednesday", 28 | "Thu" => "Thursday", 29 | "Fri" => "Friday", 30 | "Sat" => "Saturday", 31 | "Sun" => "Sunday" 32 | ]; 33 | } else { 34 | $days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; 35 | } 36 | 37 | // Generate a bunch of code 38 | $result = ""; 43 | 44 | return $result; 45 | } 46 | 47 | /** 48 | * Generates a drop down list of every hour and quarter hour. 49 | * 50 | * @param string $fieldname The name of the select tag, also the id 51 | * @param int $selectedTime The time that is preselected. Defaults to 52 | * noon 53 | * @param bool $twelve Whether to use a 24-hr or 12-hr clock 54 | * defaults to 12-hr 55 | */ 56 | function getTimeField($fieldname, $selectedTime = "720", $twelve = true) { 57 | // Generate a list of times 58 | $times = []; 59 | 60 | // Start at 0 and add 15 for every hour and every quarter hour 61 | for ($i = 0; $i <= 1440; $i += 15) { 62 | $times[] = $i; 63 | } 64 | 65 | // Now turn it into a bunch of code 66 | $result = ""; 72 | 73 | return $result; 74 | } 75 | 76 | /** 77 | * Swaps a day's format. If it's numerical, you get a 3 letter string. If it's 78 | * a 3(or 4) letter string, you get a number back. 79 | * If it can't figure it out, you'll get Sunday. 80 | * 81 | * @param mixed $day The day to translate 82 | * 83 | * @return mixed A numeric representation if $day is a string, a string 84 | * representation if $day is a number 85 | */ 86 | function translateDay($day) { 87 | if (is_numeric($day)) { 88 | switch ($day) { 89 | case 1: 90 | $day = 'Mon'; 91 | break; 92 | case 2: 93 | $day = 'Tue'; 94 | break; 95 | case 3: 96 | $day = 'Wed'; 97 | break; 98 | case 4: 99 | $day = 'Thur'; 100 | break; 101 | case 5: 102 | $day = 'Fri'; 103 | break; 104 | case 6: 105 | $day = 'Sat'; 106 | break; 107 | case 7: 108 | default: 109 | $day = 'Sun'; 110 | break; 111 | } 112 | } else { 113 | switch ($day) { 114 | case 'Mon': 115 | $day = 1; 116 | break; 117 | case 'Tue': 118 | $day = 2; 119 | break; 120 | case 'Wed': 121 | $day = 3; 122 | break; 123 | case 'Thur': 124 | case 'Thu': 125 | $day = 4; 126 | break; 127 | case 'Fri': 128 | $day = 5; 129 | break; 130 | case 'Sat': 131 | $day = 6; 132 | break; 133 | case 'Sun': 134 | default: 135 | $day = 0; 136 | break; 137 | } 138 | } 139 | return $day; 140 | } 141 | 142 | /** 143 | * Translates from numeric, minute time to the user friendly hr:mn AM/PM 144 | * 145 | * @param int $time The time to translate 146 | * @param bool $twelve Whether to use a 12-hr or 24-hr clock. Defaults 147 | * to a 12-hr clock 148 | * 149 | * @return string The time translated 150 | * @throws Exception Thrown if the time provided is not numeric. 151 | */ 152 | function translateTime($time, $twelve = true) { 153 | if (is_numeric($time)) { 154 | // Generate a 12-hour time if it is requested 155 | if ($twelve) { 156 | if ($time >= 720 && $time < 1440) { 157 | $twelve = " pm"; 158 | } else { 159 | $twelve = " am"; 160 | } 161 | 162 | if ($time >= 780) { 163 | $time -= 720; 164 | } elseif ($time < 60) { 165 | $time += 720; 166 | } 167 | } else { 168 | $twelve = ""; 169 | } 170 | 171 | // Calculate the hour and the minute 172 | $hr = floor($time / 60); 173 | $mn = str_pad($time % 60, 2, "0"); 174 | return "{$hr}:{$mn}{$twelve}"; 175 | } else { 176 | throw new Exception("FUCK OFF! IT's NOT IMPLEMENTED YET. IT'S 2FUCKINGAM AND I'M TIRED AND CRANKY. SO FUCK OFF!"); 177 | } 178 | } 179 | 180 | /** 181 | * Translates the time provided by the dump file into the format needed 182 | * by the database 183 | * 184 | * @param int $time The time as provided by the dump file 185 | * 186 | * @return int The time as formatted for the database (ie: number of 187 | * minutes into the day 188 | */ 189 | function translateTimeDump($time) { 190 | $hour = substr($time, 0, 2); 191 | $min = substr($time, -2); 192 | return ($hour * 60) + $min; 193 | } 194 | 195 | /** 196 | * Returns the action requested by api endpoint 197 | * 198 | * @return null | string 199 | */ 200 | function getAction() { 201 | 202 | // The path is exploded into ['', 'api', 'CONTROLLER' 'ACTION'] 203 | $path = explode('/', $_SERVER['REQUEST_URI']); 204 | // What action are we performing today? 205 | if (empty($path[2])) { 206 | return null; 207 | } else { 208 | return $path[2]; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 46 | 47 | 48 | 49 | <?= (!empty($TITLE)) ? $TITLE . " - " : "" ?>Schedule Maker 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ScheduleMaker" /> 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | 98 |
99 |
100 |
101 |
102 |
103 |
CSH
104 | Version: | Help | Status | Report Issues 105 |
106 | Development v3.4: Mary Strodl (mstrodl at csh.rit.edu)
107 | Development v3.1-3.3: Devin Matte (matted at csh.rit.edu)
108 | Development v3: Ben Grawi (bgrawi at csh.rit.edu)
109 | Development v2: Ben Russell (benrr101 at csh.rit.edu)
110 | Idea: John Resig (phytar at csh.rit.edu)
111 | Hosting: Computer Science House
112 |
113 |
114 |
115 | 119 |
120 | 121 | 130 | 134 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#2B4E69", 3 | "description": "A course database lookup tool and schedule building web application for use at Rochester Institute of Technology.", 4 | "dir": "auto", 5 | "display": "standalone", 6 | "icons": [ 7 | { 8 | "sizes": "48x48 72x72 96x96 128x128 256x256 512x512", 9 | "type": "image\/svg", 10 | "src": "/img/csh_logo_square.svg" 11 | } 12 | ], 13 | "lang": "en", 14 | "name": "CSH ScheduleMaker", 15 | "orientation": "any", 16 | "short_name": "ScheduleMaker", 17 | "start_url": "/", 18 | "theme_color": "#2C3E50" 19 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "schedulemaker", 3 | "version": "3.5.0", 4 | "private": true, 5 | "description": "A course database lookup tool and schedule building web application for use at Rochester Institute of Technology.", 6 | "main": "index.php", 7 | "scripts": { 8 | "build": "gulp build", 9 | "lint": "eslint assets/src/**/*.ts", 10 | "typecheck": "tsc --noEmit" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/ComputerScienceHouse/schedulemaker.git" 15 | }, 16 | "keywords": [ 17 | "csh", 18 | "ComputerScienceHouse", 19 | "schedule", 20 | "maker", 21 | "schedulemaker", 22 | "rit", 23 | "Rochester Institute of Technology" 24 | ], 25 | "contributors": [ 26 | { 27 | "name": "Devin Matte", 28 | "email": "matted@csh.rit.edu" 29 | }, 30 | { 31 | "name": "Ben Grawi", 32 | "email": "bgrawi@csh.rit.edu" 33 | }, 34 | { 35 | "name": "Ben Russell", 36 | "email": "benrr101@csh.rit.edu" 37 | }, 38 | { 39 | "name": "John Resig", 40 | "email": "phytar@csh.rit.edu" 41 | } 42 | ], 43 | "license": "GPL-2.0", 44 | "bugs": { 45 | "url": "https://github.com/ComputerScienceHouse/schedulemaker/issues" 46 | }, 47 | "config": { 48 | "stateVersion": 1 49 | }, 50 | "homepage": "https://schedule.csh.rit.edu", 51 | "devDependencies": { 52 | "@datadog/browser-rum": "^4.8.1", 53 | "@types/angular": "1.5", 54 | "@types/google.analytics": "0.0.42", 55 | "@types/mousetrap": "^1.6.9", 56 | "@typescript-eslint/eslint-plugin": "^5.21.0", 57 | "@typescript-eslint/parser": "^5.21.0", 58 | "del": "^6.0.0", 59 | "eslint": "^8.14.0", 60 | "eslint-config-standard": "^17.0.0", 61 | "eslint-plugin-import": "^2.26.0", 62 | "eslint-plugin-n": "^15.2.0", 63 | "eslint-plugin-node": "^11.1.0", 64 | "eslint-plugin-promise": "^6.0.0", 65 | "eslint-plugin-standard": "^5.0.0", 66 | "event-stream": "^4.0.1", 67 | "gulp": "^4.0.2", 68 | "gulp-concat": "^2.6.1", 69 | "gulp-htmlmin": "^5.0.1", 70 | "gulp-jshint": "^2.1.0", 71 | "gulp-less": "^5.0.0", 72 | "gulp-minify-css": "^1.2.4", 73 | "gulp-ng-annotate": "^2.1.0", 74 | "gulp-rename": "^2.0.0", 75 | "gulp-replace": "^1.1.3", 76 | "gulp-sourcemaps": "^3.0.0", 77 | "gulp-template": "^5.0.0", 78 | "gulp-typescript": "^5.0.1", 79 | "gulp-uglify": "^2.0.0", 80 | "install": "^0.13.0", 81 | "typescript": "^4.6.4", 82 | "vinyl-paths": "^3.0.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /processDump.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.4 2 | 3 | RUN docker-php-ext-install mysqli 4 | 5 | WORKDIR /app 6 | 7 | COPY inc ./inc 8 | 9 | COPY inc/config.env.php inc/config.php 10 | 11 | COPY tools ./tools 12 | 13 | ENTRYPOINT ["php", "/app/tools/processDump.php", "-d"] 14 | 15 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /schedule.php 3 | Disallow: /browse.php 4 | Disallow: /search.php 5 | -------------------------------------------------------------------------------- /schema/eer_diagram.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ComputerScienceHouse/schedulemaker/52f58d92c9dc3ee245a48fe9a78319c081a23b6a/schema/eer_diagram.pdf -------------------------------------------------------------------------------- /schema/migrationScripts/1.2.1to1.3-BldgCodes.php: -------------------------------------------------------------------------------- 1 | ([0-9]{3}(?:[A-Z])?)<\/td>\s*(.*)<\/td>\s*(.*)<\/td>/m"; 37 | if(!preg_match_all($regex, $web, $out, PREG_SET_ORDER)) { 38 | echo("*** FUCK. Nothing matched\n"); 39 | } 40 | foreach($out as $bldg) { 41 | $number = mysqli_real_escape_string($dbc, $bldg[1]); 42 | if(!is_numeric($number) && strlen($number) > 3) { 43 | $number = substr($number, -3); 44 | }elseif(is_numeric($number) && $number < 100) { 45 | $number = substr($number, -2); 46 | } 47 | $code = mysqli_real_escape_string($dbc, $bldg[2]); 48 | $name = mysqli_real_escape_string($dbc, $bldg[3]); 49 | 50 | // Create a query for each building 51 | $query = "INSERT INTO buildings (number, code, name) "; 52 | $query .= "VALUES('{$number}', '{$code}', '{$name}')"; 53 | 54 | // Verify! 55 | if(!mysqli_query($dbc, $query)) { 56 | echo("*** SHIT. '{$number}','{$code}','{$name}'\n"); 57 | echo("*** " . mysqli_error($dbc) . "\n"); 58 | } else { 59 | echo("... Adding {$number}, {$code}, {$name}\n"); 60 | } 61 | } 62 | 63 | // Insert manual buildings 64 | $manEnts = array( 65 | array("OFF", "OFF", "Off-Site"), 66 | array("DUB", "DUB", "Dubai"), 67 | array("TBA", "TBA", "To Be Announced")); // TBD NEEDS SPECIAL ATTENTION 68 | foreach($manEnts as $entry) { 69 | $query = "INSERT INTO buildings (number, code, name) VALUES('{$entry[0]}','{$entry[1]}','{$entry[2]}')"; 70 | if(!mysqli_query($dbc, $query)) { 71 | echo("*** Failed to insert manual building entries.\n***".mysqli_error($dbc)); 72 | mysqli_rollback($dbc); 73 | die(); 74 | } 75 | } 76 | 77 | // TBD=TBA 78 | $query = "UPDATE times SET building = 'TBA' WHERE building='TBD'"; 79 | if(!mysqli_query($dbc, $query)) { 80 | echo("*** Failed to change TBD to TBA\n*** ".mysqli_error($dbc)."\n"); 81 | mysqli_rollback($dbc); 82 | die(); 83 | } 84 | 85 | // Add a field for the building type for stored schedules 86 | $query = "ALTER TABLE schedules ADD COLUMN (`building` "; 87 | $query .= "SET('code','number') DEFAULT 'number')"; 88 | if(!mysqli_query($dbc, $query)) { 89 | echo("*** Failed to add column to schedules\n"); 90 | echo("*** " . mysqli_error($dbc) . "\n"); 91 | mysqli_rollback($dbc); 92 | die(); 93 | } 94 | 95 | // Anything that has a blank oldId set the type to code 96 | $query = "UPDATE schedules SET building='code' WHERE oldid=''"; 97 | if(!mysqli_query($dbc, $query)) { 98 | echo("*** Failed to update saved schedule building style\n"); 99 | echo("*** " . mysqli_error($dbc) . "\n"); 100 | mysqli_rollback($dbc); 101 | die(); 102 | } 103 | 104 | // Now we need to change existing courses to have correct building 105 | $query = "SELECT code, number FROM buildings"; 106 | $r = mysqli_query($dbc, $query); 107 | if(!$r) { 108 | echo("*** Failed to lookup the buildings.\n***" . mysqli_error($dbc) . "\n"); 109 | mysqli_rollback($dbc); 110 | die(); 111 | } 112 | 113 | $bldg = array(); 114 | while($row = mysqli_fetch_assoc($r)) { 115 | $bldg[$row['code']] = $row['number']; 116 | } 117 | mysqli_free_result($r); 118 | 119 | $query = "SELECT DISTINCT(building) FROM times WHERE building REGEXP('[0-9]{3}') OR building REGEXP('[A-Z]{3}')"; 120 | $r = mysqli_query($dbc, $query); 121 | if(!$r) { 122 | echo("*** Failed to lookup buildings that need conversion\n***".mysqli_error($dbc)."\n"); 123 | mysqli_rollback($dbc); 124 | die(); 125 | } 126 | 127 | while($row = mysqli_fetch_assoc($r)) { 128 | if(is_numeric($row['building']) && strlen($row['building']) && $row['building'] < 100) { 129 | $building = substr($row['building'], -2); 130 | } elseif(!is_numeric($row['building']) && !empty($bldg[$row['building']])) { 131 | $building = $bldg[$row['building']]; 132 | } else { 133 | continue; 134 | } 135 | $query = "UPDATE times SET building='{$building}' WHERE building='{$row['building']}'"; 136 | if(!mysqli_query($dbc, $query)) { 137 | echo("*** Failed to update building.\n***".mysqli_error($dbc)."\n"); 138 | mysqli_rollback($dbc); 139 | die(); 140 | } 141 | } 142 | 143 | // Anything left is unknown 144 | $query = "INSERT INTO buildings (code, number, name) "; 145 | $query .= "SELECT building, building, 'UNKNOWN' FROM times WHERE building REGEXP('[A-Za-z]{3}') AND building NOT IN(SELECT code FROM buildings) GROUP BY building"; 146 | if(!mysqli_query($dbc, $query)) { 147 | echo("*** Failed to handle unknown buildings\n*** ".mysqli_error($dbc)."\n"); 148 | mysqli_rollback($dbc); 149 | die(); 150 | } 151 | 152 | 153 | mysqli_commit($dbc); 154 | mysqli_autocommit($dbc, true); 155 | echo("SUCCESS, BITCHES.\n"); 156 | -------------------------------------------------------------------------------- /schema/migrationScripts/1.2.1to1.3-ScheduleQuarters.php: -------------------------------------------------------------------------------- 1 | quarter .\n***" . mysqli_error($dbc) . "\n"); 36 | mysqli_rollback($dbc); 37 | die(); 38 | } 39 | echo("... AND IT'S DONE\n"); 40 | 41 | // Update for each of those records 42 | $bldg = array(); 43 | while($row = mysqli_fetch_assoc($r)) { 44 | $query2 = "UPDATE schedules SET quarter = {$row['quarter']} WHERE id={$row['id']}"; 45 | $r2 = mysqli_query($dbc, $query2); 46 | if(!$r2) { 47 | echo("*** Failed to update quarter for schedule\n***" . mysqli_error($dbc) ."\n"); 48 | } 49 | } 50 | mysqli_free_result($r); 51 | mysqli_commit($dbc); 52 | mysqli_autocommit($dbc, true); 53 | echo("SUCCESS, BITCHES.\n"); 54 | -------------------------------------------------------------------------------- /schema/migrationScripts/1.4to1.5-innodb.sql: -------------------------------------------------------------------------------- 1 | -- Switch all the tables to innodb 2 | SELECT 'Changing db Engines' AS 'status'; 3 | ALTER TABLE `buildings` ENGINE = InnoDB; 4 | ALTER TABLE `courses` ENGINE = InnoDB; 5 | ALTER TABLE `departments` ENGINE = InnoDB; 6 | ALTER TABLE `quarters` ENGINE = InnoDB; 7 | ALTER TABLE `schedulecourses` ENGINE = InnoDB; 8 | ALTER TABLE `schedulenoncourses` ENGINE = InnoDB; 9 | ALTER TABLE `schedules` ENGINE = InnoDB; 10 | ALTER TABLE `schools` ENGINE = InnoDB; 11 | ALTER TABLE `scrapelog` ENGINE = InnoDB; 12 | ALTER TABLE `sections` ENGINE = InnoDB; 13 | ALTER TABLE `times` ENGINE = InnoDB; 14 | 15 | -- Buildings --------------------------------------------------------------- 16 | SELECT 'Fixing Building Table' AS 'status'; 17 | ALTER TABLE `times` ADD INDEX ( `building` ); 18 | ALTER TABLE `times` 19 | CHANGE `building` `building` VARCHAR( 4 ) CHARACTER SET latin1 20 | COLLATE latin1_swedish_ci NULL DEFAULT NULL 21 | COMMENT 'building number bitches!'; 22 | ALTER TABLE `buildings` 23 | CHANGE `code` `code` VARCHAR( 4 ) CHARACTER SET latin1 24 | COLLATE latin1_swedish_ci NULL DEFAULT NULL; 25 | ALTER TABLE `buildings` 26 | CHANGE `number` `number` VARCHAR( 4 ) CHARACTER SET latin1 27 | COLLATE latin1_swedish_ci NOT NULL; 28 | UPDATE times SET building = NULL WHERE building = ""; 29 | 30 | -- Update broken building codes 31 | UPDATE `buildings` SET `name` = 'American College of Management and Technology', number="ACMT", code="ACMT" WHERE `buildings`.`number` = 'ACM'; 32 | UPDATE `buildings` SET `name` = 'American University in Kosovo' WHERE `buildings`.`number` = 'AUK'; 33 | UPDATE `buildings` SET `name` = 'Online', number='ON', code="ON" WHERE `buildings`.`number` = 'ONL'; 34 | INSERT INTO `buildings` (`number`, `code`, `name`) VALUES ('OFFC', 'OFFC', 'UNKNOWN'); 35 | INSERT INTO `buildings` (`number`, `code`, `name`) VALUES ('07', '07', 'Gannet/Booth Hall'); 36 | INSERT INTO `buildings` (`number`, `code`, `name`) VALUES ('73', 'INS', 'Institute Hall'); 37 | 38 | -- Prune bad times from olden days 39 | DELETE FROM times WHERE day=0 AND start=0 AND end=0; -- Prune bad times from old days 40 | 41 | -- Fix bad building numbers 42 | UPDATE times SET building = TRIM(building); 43 | UPDATE times SET building = CONCAT("0",building) WHERE LENGTH(building) = 1 AND building NOT IN(SELECT number FROM buildings); 44 | UPDATE times SET building = CONCAT("0",building) WHERE LENGTH(building) = 2 AND building NOT IN(SELECT number FROM buildings); 45 | UPDATE times SET building = SUBSTR(building, -2) 46 | WHERE LENGTH(building) = 3 47 | AND building NOT IN(SELECT number FROM buildings) 48 | AND building LIKE "0%"; 49 | UPDATE times SET building='TBA' WHERE building='TBD'; 50 | UPDATE times SET building='ACMT' WHERE building='CMT'; 51 | UPDATE times SET building='OFF' WHERE building='FF'; 52 | UPDATE times SET building='DUB' WHERE building='NA'; 53 | UPDATE times SET building='ON' WHERE building='INE'; 54 | UPDATE times SET building='OFFC' WHERE building='FFC'; 55 | UPDATE times SET building='ACMT' WHERE building='ACM'; 56 | UPDATE times SET building=NULL WHERE building IN("115", "12A", "15A", "1A", "550", "78A", "83", "8A", "950", "00", "1A"); 57 | 58 | ALTER TABLE `times` ADD FOREIGN KEY ( `building` ) 59 | REFERENCES `buildings` (`number`) 60 | ON DELETE SET NULL ON UPDATE CASCADE; 61 | 62 | -- Time to section 63 | SELECT 'Times->Sections' AS 'status'; 64 | ALTER TABLE `times` ADD FOREIGN KEY ( `section` ) 65 | REFERENCES `sections` (`id`) 66 | ON DELETE CASCADE ON UPDATE CASCADE; 67 | 68 | -- Section to courses 69 | SELECT 'Sections->Courses' AS 'status'; 70 | DELETE FROM sections WHERE course NOT IN(SELECT id FROM courses); 71 | ALTER TABLE `sections` ADD FOREIGN KEY ( `course` ) 72 | REFERENCES `courses` (`id`) 73 | ON DELETE CASCADE ON UPDATE CASCADE; 74 | 75 | -- Schools ----------------------------------------------------------------- 76 | SELECT 'Fixing Schools' AS 'status'; 77 | ALTER TABLE `schools` CHANGE `id` `number` VARCHAR( 2 ) NULL DEFAULT NULL; 78 | ALTER TABLE `schools` CHANGE `code` `code` VARCHAR( 8 ) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL; 79 | UPDATE schools SET code = NULL; 80 | ALTER TABLE schools DROP PRIMARY KEY; 81 | ALTER TABLE `schools` ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; 82 | ALTER TABLE `schools` ADD UNIQUE `UNI_id-number` ( `number` , `code` ); 83 | 84 | -- Fix the departments table 85 | SELECT 'Fixing Departments' AS 'status'; 86 | ALTER TABLE `departments` CHANGE `id` `number` SMALLINT( 4 ) UNSIGNED ZEROFILL NULL DEFAULT NULL; 87 | ALTER TABLE `departments` CHANGE `code` `code` VARCHAR( 5 ) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL; 88 | ALTER TABLE `departments` DROP PRIMARY KEY; 89 | ALTER TABLE `departments` ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; 90 | ALTER TABLE `departments` CHANGE `number` `number` SMALLINT( 4 ) UNSIGNED ZEROFILL NULL DEFAULT NULL; 91 | UPDATE departments SET code=NULL WHERE code='0'; 92 | 93 | -- Correct the department->school stuff 94 | UPDATE departments AS d SET school = (SELECT id FROM schools AS s WHERE SUBSTRING(CONVERT(d.number, CHAR(4)), 1, 2) = s.number); 95 | ALTER TABLE `departments` CHANGE `school` `school` INT( 10 ) UNSIGNED NULL DEFAULT NULL; 96 | 97 | -- Correct the course department number 98 | ALTER TABLE `courses` ADD `departmentid` INT UNSIGNED NOT NULL AFTER `id`; 99 | UPDATE courses AS c SET departmentid = (SELECT id FROM departments AS d WHERE c.department=d.number); 100 | DELETE FROM courses WHERE departmentid=0; -- bye bye Indoor Gardening :( 101 | ALTER TABLE courses DROP INDEX department_2; 102 | ALTER TABLE courses DROP INDEX department; 103 | ALTER TABLE `courses` ADD UNIQUE `coursenumbers` ( `quarter` , `departmentid` , `course`); 104 | ALTER TABLE courses DROP COLUMN department; 105 | ALTER TABLE `courses` CHANGE `departmentid` `department` INT( 10 ) UNSIGNED NOT NULL; 106 | ALTER TABLE `courses` ADD INDEX `department` (`department`); 107 | ALTER TABLE `courses` ADD FOREIGN KEY ( `department` ) 108 | REFERENCES `departments` (`id`) 109 | ON DELETE CASCADE ON UPDATE CASCADE ; 110 | 111 | -- Department to School 112 | SELECT 'Departments->Schools' AS 'status'; 113 | ALTER TABLE `departments` ADD INDEX `school` ( `school` ); 114 | UPDATE departments SET school=NULL WHERE school="00"; 115 | ALTER TABLE `departments` ADD FOREIGN KEY ( `school` ) 116 | REFERENCES `schools` (`id`) 117 | ON DELETE CASCADE ON UPDATE CASCADE ; 118 | 119 | -- Course to Quarter 120 | SELECT 'Course->Quarter' AS 'status'; 121 | ALTER TABLE `courses` ADD INDEX `quarter` ( `quarter` ); 122 | ALTER TABLE `courses` ADD FOREIGN KEY ( `quarter` ) 123 | REFERENCES `quarters` (`quarter`) 124 | ON DELETE CASCADE ON UPDATE CASCADE ; 125 | 126 | -- Schedulecourses to schedule 127 | SELECT 'ScheduleCourses->Schedules' AS 'status'; 128 | ALTER TABLE `schedulecourses` ADD FOREIGN KEY ( `schedule` ) 129 | REFERENCES `schedules` (`id`) 130 | ON DELETE CASCADE ON UPDATE CASCADE; 131 | 132 | -- Schedulecourses to Sections 133 | SELECT 'ScheduleCourses->Sections' AS 'status'; 134 | ALTER TABLE `schedulecourses` ADD INDEX `FK_schedulecourses-sections` ( `section` ); 135 | DELETE FROM schedulecourses WHERE section NOT IN(SELECT id FROM sections); 136 | ALTER TABLE `schedulecourses` ADD FOREIGN KEY ( `section` ) 137 | REFERENCES `sections` (`id`) 138 | ON DELETE CASCADE ON UPDATE CASCADE ; -- Not sure why, but this one takes decades to run 139 | 140 | -- Schedulenoncourses to schedule 141 | SELECT 'ScheduleNonCourses->Schedules' AS 'status'; 142 | DELETE FROM schedulenoncourses 143 | WHERE schedule NOT IN (SELECT id FROM schedules ); 144 | ALTER TABLE `schedulenoncourses` ADD FOREIGN KEY ( `schedule` ) 145 | REFERENCES `schedules` (`id`) 146 | ON DELETE CASCADE ON UPDATE CASCADE ; 147 | 148 | -- Update the procedures 149 | -------------------------------------------------------------------------------- /schema/migrationScripts/1.5.1to1.6.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- Migration to version 1.6 for semester support 3 | -- 4 | -- @author Benjamin Russell (benrr101@csh.rit.edu) 5 | -- @descrip This script will perform the necessary database schema migration 6 | -- to enable robust semester support 7 | -- ------------------------------------------------------------------------- 8 | 9 | -- Step 1) Drop existing semester courses 10 | DELETE FROM courses WHERE `quarter` > 20130; 11 | 12 | -- Step 2a) Add the new column for the quarternumbers 13 | ALTER TABLE departments ADD COLUMN `qtrnums` VARCHAR(20) NULL DEFAULT NULL; 14 | 15 | -- Step 2b) Insert new records for departments under semesters 16 | INSERT INTO departments(`number`, `code`, `title`, `school`, `qtrnums`) 17 | SELECT NULL, `code`, `title`, `school`, TRIM(TRAILING ', ' FROM GROUP_CONCAT(`number` SEPARATOR ', ')) 18 | FROM departments 19 | WHERE `code` IS NOT NULL 20 | GROUP BY `code`; 21 | 22 | -- Step 2c) Update the old records to remove the codes 23 | UPDATE departments SET `code` = NULL WHERE `number` IS NOT NULL; 24 | UPDATE departments SET `qtrnums` = NULL WHERE `qtrnums` = ''; 25 | 26 | -- Step 3) Delete pesky duplicates (not sure how those got in there...) 27 | -- Adapted from http://stackoverflow.com/a/3671629 28 | DELETE d1 FROM departments AS d1 JOIN departments AS d2 ON d1.code = d2.code WHERE d1.id > d2.id; 29 | 30 | -- Step 4) Create unique keys on department codes and numbers 31 | -- NOTE: This will work b/c InnoDB allows multiple NULLs in unique columns (unlike MyISAM) 32 | ALTER TABLE departments ADD UNIQUE `UNI_deptnumber` ( `number` ); 33 | ALTER TABLE departments ADD UNIQUE `UNI_deptcode` ( `code` ); 34 | 35 | -- Step 3) Run the processDump script -------------------------------------------------------------------------------- /schema/migrationScripts/1.6.4to1.6.5.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- Migration to version 1.6.5 for CROATIA MODE support 3 | -- 4 | -- @author Benjamin Russell (benrr101@csh.rit.edu) 5 | -- @descrip This script will perform the necessary database schema migration 6 | -- to enable off campus buildings to be set 7 | -- ------------------------------------------------------------------------- 8 | 9 | -- Add the column 10 | ALTER TABLE buildings 11 | ADD off_campus BOOLEAN DEFAULT 0; 12 | 13 | -- Set known off campus buildings as such 14 | UPDATE buildings SET off_campus = 1 15 | WHERE `number` IN ( 16 | 'OFFC', 17 | 'OFF', 18 | 'DUB', 19 | 'CMT', 20 | 'AUK', 21 | 'ACMT', 22 | '93' 23 | ); 24 | 25 | -------------------------------------------------------------------------------- /schema/migrationScripts/1.7.1to1.8.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- MIGRATION SCRIPT 1.7.1 to 1.8 3 | -- 4 | -- @author Ben (benrr101@csh.rit.edu) 5 | -- @descrip DB migration script for moving from 1.7.1 to 1.8 (or higher) 6 | -- + Adding column for schedule image location 7 | -- ------------------------------------------------------------------------- 8 | 9 | ALTER TABLE schedules ADD COLUMN (`image` BOOL NOT NULL DEFAULT FALSE); -------------------------------------------------------------------------------- /schema/migrationScripts/3.0.30.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `sections` 2 | CHANGE `type` `type` ENUM ('R', 'N', 'H', 'BL', 'OL', 'O') 3 | CHARACTER SET latin1 4 | COLLATE latin1_swedish_ci NOT NULL DEFAULT 'R' 5 | COMMENT 'R=regular, N=night, OL=online, H=honors, BL=????', 6 | CHANGE `maxenroll` `maxenroll` SMALLINT(3) UNSIGNED NOT NULL 7 | COMMENT 'max enrollment', 8 | CHANGE `curenroll` `curenroll` SMALLINT(3) UNSIGNED NOT NULL 9 | COMMENT 'current enrollment', 10 | CHANGE `instructor` `instructor` VARCHAR(64) NOT NULL DEFAULT 'TBA' 11 | COMMENT 'Instructor\'s Name'; 12 | 13 | UPDATE `sections` 14 | SET `type` = 'OL' 15 | WHERE `type` = 'O'; 16 | 17 | ALTER TABLE `sections` 18 | CHANGE `type` `type` ENUM ('R', 'N', 'H', 'BL', 'OL') 19 | CHARACTER SET latin1 20 | COLLATE latin1_swedish_ci NOT NULL DEFAULT 'R' 21 | COMMENT 'R=regular, N=night, OL=online, H=honors, BL=????'; 22 | 23 | ALTER TABLE `times` 24 | CHANGE `room` `room` VARCHAR(10) 25 | CHARACTER SET latin1 26 | COLLATE latin1_swedish_ci NOT NULL 27 | COMMENT 'room number'; 28 | 29 | INSERT INTO `buildings` (`number`, `code`, `name`) VALUES ('ZAG', 'ZAG', 'Building in Croatia'); 30 | 31 | ALTER TABLE `quarters` 32 | DROP `breakstart`, 33 | DROP `breakend`; 34 | 35 | ALTER TABLE `departments` 36 | CHANGE `title` `title` VARCHAR(30) 37 | CHARACTER SET latin1 38 | COLLATE latin1_swedish_ci NULL; -------------------------------------------------------------------------------- /schema/migrationScripts/f_4charcourses.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- Migration Script for f_4charcourses 3 | -- 4 | -- @author Benjamin Russell (benrr101@csh.rit.edu) 5 | -- @descrip Migration script to implement the changes for f_4charcourses. 6 | -- basically sets the course number field to a varchar(4) to enable 7 | -- courses that have letters in their number (eg. PHYS-211A) 8 | -- ------------------------------------------------------------------------- 9 | 10 | ALTER TABLE `courses` 11 | CHANGE `course` `course` VARCHAR(4) NOT NULL; 12 | 13 | delimiter // 14 | DROP PROCEDURE IF EXISTS InsertOrUpdateCourse// 15 | CREATE PROCEDURE InsertOrUpdateCourse( 16 | IN p_quarter INT, 17 | IN p_department_num INT, 18 | IN p_department_code VARCHAR(4), 19 | IN p_course VARCHAR(4), 20 | IN p_credits INT, 21 | IN p_title VARCHAR(50), 22 | IN p_description TEXT 23 | ) 24 | BEGIN 25 | -- Determine if the course already exists 26 | DECLARE recordFound INT(1); 27 | DECLARE v_department INT(10); 28 | 29 | -- Select the department ID for the course 30 | IF p_quarter > 20130 THEN 31 | -- NOTE: This LIMIT 1 looks jank as fuck. Here's why it's important: There are some 32 | -- departments under semesters that condensed multiple departments from quarters 33 | -- (eg: CSCI is made from 4003 and 4005). That's ok. Problems arose because when deciding 34 | -- which department record would own a course under semesters, this query would return 35 | -- multiple records. Limiting it to one seems like a cop-out since (eg here) what if 36 | -- a CSCI course belongs to 4003?? The truth: It doesn't matter. By limiting it to one, we 37 | -- are storing all semester courses under the same department record (a "random" CSCI 38 | -- one not the 4003 CSCI or the 4005 CSCI). Ordering it will guarantee it's the same one each time. 39 | SET v_department = (SELECT d.id FROM departments AS d WHERE d.code = p_department_code ORDER BY d.number LIMIT 1); 40 | ELSE 41 | SET v_department = (SELECT d.id FROM departments AS d WHERE d.number = p_department_num); 42 | END IF; 43 | 44 | -- Does the record exist? 45 | SET recordFound = ( 46 | SELECT COUNT(*) 47 | FROM courses AS c 48 | WHERE c.department = v_department 49 | AND c.course = p_course 50 | AND c.quarter = p_quarter 51 | ); 52 | IF recordFound > 0 THEN 53 | -- Course exists, so update it 54 | UPDATE courses AS c 55 | SET c.title = p_title, c.description = p_description, c.credits = p_credits 56 | WHERE c.department = v_department AND course = p_course AND quarter = p_quarter; 57 | SELECT "updated" AS action; 58 | ELSE 59 | -- Course doesn't exist, so insert it 60 | INSERT INTO courses (quarter, department, course, title, description, credits) 61 | VALUES(p_quarter, v_department, p_course, p_title, p_description, p_credits); 62 | SELECT "inserted" AS action; 63 | END IF; 64 | 65 | -- Return the id of the course 66 | SELECT c.id FROM courses AS c WHERE c.department = v_department AND c.course = p_course AND c.quarter = p_quarter; 67 | END// -------------------------------------------------------------------------------- /schema/procedures/InsertOrUpdateCourse.sql: -------------------------------------------------------------------------------- 1 | delimiter // 2 | DROP PROCEDURE IF EXISTS InsertOrUpdateCourse// 3 | CREATE PROCEDURE InsertOrUpdateCourse( 4 | IN p_quarter INT, 5 | IN p_department_num INT, 6 | IN p_department_code VARCHAR(4), 7 | IN p_course VARCHAR(4), 8 | IN p_credits INT, 9 | IN p_title VARCHAR(50), 10 | IN p_description TEXT 11 | ) 12 | BEGIN 13 | -- Determine if the course already exists 14 | DECLARE recordFound INT(1); 15 | DECLARE v_department INT(10); 16 | 17 | -- Select the department ID for the course 18 | IF p_quarter > 20130 THEN 19 | -- NOTE: This LIMIT 1 looks jank as fuck. Here's why it's important: There are some 20 | -- departments under semesters that condensed multiple departments from quarters 21 | -- (eg: CSCI is made from 4003 and 4005). That's ok. Problems arose because when deciding 22 | -- which department record would own a course under semesters, this query would return 23 | -- multiple records. Limiting it to one seems like a cop-out since (eg here) what if 24 | -- a CSCI course belongs to 4003?? The truth: It doesn't matter. By limiting it to one, we 25 | -- are storing all semester courses under the same department record (a "random" CSCI 26 | -- one not the 4003 CSCI or the 4005 CSCI). Ordering it will guarantee it's the same one each time. 27 | SET v_department = (SELECT d.id FROM departments AS d WHERE d.code = p_department_code ORDER BY d.number LIMIT 1); 28 | ELSE 29 | SET v_department = (SELECT d.id FROM departments AS d WHERE d.number = p_department_num); 30 | END IF; 31 | 32 | -- Does the record exist? 33 | SET recordFound = ( 34 | SELECT COUNT(*) 35 | FROM courses AS c 36 | WHERE c.department = v_department 37 | AND c.course = p_course 38 | AND c.quarter = p_quarter 39 | ); 40 | IF recordFound > 0 THEN 41 | -- Course exists, so update it 42 | UPDATE courses AS c 43 | SET c.title = p_title, c.description = p_description, c.credits = p_credits 44 | WHERE c.department = v_department AND course = p_course AND quarter = p_quarter; 45 | SELECT "updated" AS action; 46 | ELSE 47 | -- Course doesn't exist, so insert it 48 | INSERT INTO courses (quarter, department, course, title, description, credits) 49 | VALUES(p_quarter, v_department, p_course, p_title, p_description, p_credits); 50 | SELECT "inserted" AS action; 51 | END IF; 52 | 53 | -- Return the id of the course 54 | SELECT c.id FROM courses AS c WHERE c.department = v_department AND c.course = p_course AND c.quarter = p_quarter; 55 | END 56 | -------------------------------------------------------------------------------- /schema/procedures/InsertOrUpdateSection.sql: -------------------------------------------------------------------------------- 1 | delimiter // 2 | DROP PROCEDURE IF EXISTS InsertOrUpdateSection// 3 | CREATE PROCEDURE InsertOrUpdateSection( 4 | IN p_course INT, 5 | IN p_section VARCHAR(4), 6 | IN p_title VARCHAR(50), 7 | IN p_instructor VARCHAR(64), 8 | IN p_type VARCHAR(2), 9 | IN p_status VARCHAR(1), 10 | IN p_maxenroll INT, 11 | IN p_curenroll INT 12 | ) 13 | BEGIN 14 | -- Attempt to find the section 15 | DECLARE recordfound INT(1); 16 | SET recordfound = (SELECT COUNT(id) FROM sections WHERE course = p_course AND section = p_section); 17 | 18 | IF recordfound > 0 THEN 19 | -- Section exists, so just update it 20 | UPDATE sections 21 | SET title = p_title, 22 | instructor = p_instructor, 23 | type = p_type, 24 | status = p_status, 25 | maxenroll = p_maxenroll, 26 | curenroll = p_curenroll 27 | WHERE course = p_course AND section = p_section; 28 | SELECT "updated" AS action; 29 | ELSE 30 | -- Section does not exist, so insert it 31 | INSERT INTO sections (course, section, title, instructor, type, status, maxenroll, curenroll) 32 | VALUES(p_course, p_section, p_title, p_instructor, p_type, p_status, p_maxenroll, p_curenroll); 33 | SELECT "inserted" AS action; 34 | END IF; 35 | 36 | -- Get the id of the section we just inserted or updated 37 | SELECT id FROM sections WHERE course = p_course AND section = p_section; 38 | END// 39 | -------------------------------------------------------------------------------- /schema/schema_model.mwb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ComputerScienceHouse/schedulemaker/52f58d92c9dc3ee245a48fe9a78319c081a23b6a/schema/schema_model.mwb -------------------------------------------------------------------------------- /schema/tables/buildings.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- BUILDING LOOKUP TABLE 3 | -- 4 | -- @author Benjamin Russell (benrr101@csh.rit.edu) 5 | -- @descrip This table holds building codes and links them to numbers and 6 | -- to the name of the building 7 | -- ------------------------------------------------------------------------- 8 | 9 | DROP TABLE IF EXISTS buildings; 10 | CREATE TABLE buildings ( 11 | `number` VARCHAR(5) PRIMARY KEY, 12 | `code` VARCHAR(5) UNIQUE, 13 | `name` VARCHAR(100), 14 | `off_campus` BOOLEAN DEFAULT TRUE 15 | ) Engine=InnoDb; 16 | -------------------------------------------------------------------------------- /schema/tables/courses.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- Courses table 3 | -- 4 | -- @author Benjamin Russell (benrr101@csh.rit.edu) 5 | -- @descrip Table for courses. These are linked to departments and quarters 6 | -- in a one quarter/department to many courses. These are also linked 7 | -- to sections in a one course to many sections fashion. 8 | -- ------------------------------------------------------------------------- 9 | 10 | -- TABLE CREATION ---------------------------------------------------------- 11 | CREATE TABLE courses ( 12 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 13 | `quarter` SMALLINT UNSIGNED NOT NULL, 14 | `department` INT UNSIGNED NOT NULL, 15 | `course` VARCHAR(4) NOT NULL, 16 | `credits` TINYINT(2) UNSIGNED NOT NULL DEFAULT 0, 17 | `title` VARCHAR(50) NOT NULL, 18 | `description` TEXT NOT NULL 19 | )ENGINE=InnoDb; 20 | 21 | -- INDEXING ---------------------------------------------------------------- 22 | ALTER TABLE `courses` 23 | ADD CONSTRAINT UQ_courses_quarter_department_course 24 | UNIQUE (`quarter`, `department`, `course`); 25 | 26 | -- FOREIGN KEYS ------------------------------------------------------------ 27 | ALTER TABLE `courses` 28 | ADD FOREIGN KEY FK_courses_quarter(`quarter`) 29 | REFERENCES `quarters`(`quarter`) 30 | ON DELETE CASCADE 31 | ON UPDATE CASCADE; 32 | 33 | ALTER TABLE `courses` 34 | ADD FOREIGN KEY FK_courses_dept(`department`) 35 | REFERENCES `departments`(`id`) 36 | ON DELETE CASCADE 37 | ON UPDATE CASCADE; -------------------------------------------------------------------------------- /schema/tables/departments.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- DEPARTMENT TABLE 3 | -- 4 | -- @author Benjamin Russell (benrr101@csh.rit.edu) 5 | -- @descrip This table holds department codes and numbers. We want to have 6 | -- one record for a department in semesters and one record for a 7 | -- department in quarters 8 | -- ------------------------------------------------------------------------- 9 | 10 | DROP TABLE IF EXISTS departments; 11 | CREATE TABLE departments ( 12 | `id` INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, 13 | `school` INT UNSIGNED NOT NULL, 14 | `number` SMALLINT(4) UNSIGNED ZEROFILL NULL DEFAULT NULL, 15 | `code` VARCHAR(4) NULL DEFAULT NULL, 16 | `title` VARCHAR(100) NOT NULL, 17 | `qtrnums` VARCHAR(20) NULL DEFAULT NULL -- Group of corresponding department numbers from quarters 18 | ) Engine=InnoDb; 19 | 20 | -- UNIQUE CONSTRAINTS ------------------------------------------------------ 21 | ALTER TABLE `departments` 22 | ADD CONSTRAINT UQ_departments_number 23 | UNIQUE (`number`); 24 | 25 | ALTER TABLE `departments` 26 | ADD CONSTRAINT UQ_departments_code 27 | UNIQUE (`code`); 28 | 29 | -- FOREIGN KEYS 30 | ALTER TABLE departments ADD INDEX departments(school); 31 | ALTER TABLE departments ADD CONSTRAINT fk_school FOREIGN KEY departments(school) 32 | REFERENCES schools(id) 33 | ON UPDATE CASCADE 34 | ON DELETE CASCADE; -------------------------------------------------------------------------------- /schema/tables/quarters.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- Quarters Table 3 | -- 4 | -- @author Benjamin Russell (benrr101@csh.rit.edu) 5 | -- @descrip Table for quarters. Although RIT changed their formatting for quarters 6 | -- we convert their new format (2135) to the old format (20135) to 7 | -- preserve sorting. 8 | -- ------------------------------------------------------------------------- 9 | 10 | -- CREATE TABLE ------------------------------------------------------------ 11 | CREATE TABLE quarters ( 12 | `quarter` SMALLINT(5) UNSIGNED NOT NULL PRIMARY KEY, 13 | `start` DATE NOT NULL, 14 | `end` DATE NOT NULL 15 | ) ENGINE=InnoDb; -------------------------------------------------------------------------------- /schema/tables/schedulecourses.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- Saved Schedule Sections 3 | -- 4 | -- @author Benjamin Russell (benrr101@csh.rit.edu) 5 | -- @descrip Table for linking sections with saved schedules. 6 | -- ------------------------------------------------------------------------- 7 | 8 | -- CREATE TABLE ------------------------------------------------------------ 9 | CREATE TABLE schedulecourses ( 10 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 11 | `schedule` INT UNSIGNED NOT NULL, 12 | `section` INT UNSIGNED NOT NULL 13 | )ENGINE=InnoDb; 14 | 15 | -- FOREIGN KEY CONSTRAINTS ------------------------------------------------- 16 | ALTER TABLE `schedulecourses` 17 | ADD FOREIGN KEY FK_schedcourses_schedule(`schedule`) 18 | REFERENCES `schedules`(`id`) 19 | ON DELETE CASCADE 20 | ON UPDATE CASCADE; 21 | 22 | ALTER TABLE `schedulecourses` 23 | ADD FOREIGN KEY FK_schedcourses_section(`section`) 24 | REFERENCES `sections`(`id`) 25 | ON DELETE CASCADE 26 | ON UPDATE CASCADE; -------------------------------------------------------------------------------- /schema/tables/schedulenoncourses.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- Saved Schedule Non-Course Items 3 | -- 4 | -- @author Benjamin Russell (benrr101@csh.rit.edu) 5 | -- @descrip Table for storing non-course items for saved schedules and linking 6 | -- them to saved schedules. 7 | -- ------------------------------------------------------------------------- 8 | 9 | CREATE TABLE schedulenoncourses ( 10 | `id` INT UNSIGNED NOT NULL PRIMARY KEY, 11 | `schedule` INT UNSIGNED NOT NULL, 12 | `title` VARCHAR(30) NOT NULL, 13 | `day` TINYINT(1) UNSIGNED NOT NULL, 14 | `start` SMALLINT(4) UNSIGNED NOT NULL, 15 | `end` SMALLINT(4) UNSIGNED NOT NULL 16 | )ENGINE=InnoDb; 17 | 18 | -- FOREIGN KEY REFERENCES -------------------------------------------------- 19 | ALTER TABLE `schedulenoncourses` 20 | ADD FOREIGN KEY FK_schednoncourses_schedule(`schedule`) 21 | REFERENCES `schedules`(`id`) 22 | ON DELETE CASCADE 23 | ON UPDATE CASCADE; -------------------------------------------------------------------------------- /schema/tables/schedules.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- SAVED SCHEDULE TABLE 3 | -- 4 | -- @author Benjamin Russell (benrr101@csh.rit.edu) 5 | -- @descrip A table for storing saved schedule records. 6 | -- ------------------------------------------------------------------------- 7 | 8 | CREATE TABLE schedules ( 9 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, -- Will be displayed to user as hex 10 | -- @TODO Safely remove this column when old schedules have been pruned 11 | `oldid` VARCHAR(7) NULL DEFAULT NULL COLLATE latin1_general_cs, -- Old index from Resig's schedule maker. It's case sensitive 12 | `datelastaccessed` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Last date accessed. Used for determining when to prune. 13 | `startday` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1, -- Start day for schedule. 0 = Sunday, 1 = Monday, etc 14 | `endday` TINYINT(1) UNSIGNED NOT NULL DEFAULT 6, -- End day for schedule. See above. 15 | -- @TODO Safely remove the zerofill from these columns 16 | `starttime` SMALLINT(4) UNSIGNED ZEROFILL NOT NULL DEFAULT 0480, -- Start time for schedule. Value is minutes into the day (eg. 0=midnight, 480=8AM) 17 | `endtime` SMALLINT(4) UNSIGNED ZEROFILL NOT NULL DEFAULT 1320, -- End time for schedule. Value is minutes into the day (eg. 1320=10PM) 18 | `building` SET('code', 'number') NOT NULL DEFAULT 'number', -- Whether to show old or new building id. Defaulting to number for old fogies 19 | `quarter` SMALLINT(5) UNSIGNED NULL DEFAULT NULL, -- The quarter the schedule was made for. Not necessary for it to reference quarters table 20 | `image` BOOL NOT NULL DEFAULT FALSE -- Whether or not an image of the schedule has been generated and saved 21 | )ENGINE=InnoDb; 22 | 23 | -- Add index to searchable columns 24 | ALTER TABLE schedules ADD INDEX (`oldid`); 25 | 26 | -- FOREIGN KEYS ------------------------------------------------------------ 27 | ALTER TABLE `schedules` 28 | ADD FOREIGN KEY FK_schedules_quarter(`quarter`) 29 | REFERENCES `quarters`(`quarter`) 30 | ON UPDATE CASCADE 31 | ON DELETE SET NULL; -------------------------------------------------------------------------------- /schema/tables/schools.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- SCHOOL LOOKUP TABLE 3 | -- 4 | -- @author Benjamin Russell (benrr101@csh.rit.edu) 5 | -- @descrip This table holds school codes and links them to numbers and 6 | -- to the name of the school 7 | -- ------------------------------------------------------------------------- 8 | 9 | -- TABLE CREATION ---------------------------------------------------------- 10 | CREATE TABLE IF NOT EXISTS `schools` ( 11 | `id` INT UNSIGNED NOT NULL PRIMARY KEY, 12 | `number` tinyint(2) UNSIGNED ZEROFILL NULL DEFAULT NULL, 13 | `code` VARCHAR(5) NULL DEFAULT NULL, 14 | `title` varchar(30) NOT NULL, 15 | PRIMARY KEY (`id`) 16 | ) ENGINE=InnoDb; 17 | 18 | -- ADD INDEXES ------------------------------------------------------------- 19 | ALTER TABLE `schools` 20 | ADD CONSTRAINT UQ_schools_number_code 21 | UNIQUE (`number`, `code`); 22 | -------------------------------------------------------------------------------- /schema/tables/scrapelog.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- Scrape Log Table 3 | -- 4 | -- @author Benjamin Russell (benrr101@csh.rit.edu) 5 | -- @descrip Rudimentary table for storing the status of past dump processing 6 | -- cron jobs. 7 | -- ------------------------------------------------------------------------- 8 | 9 | -- CREATE TABLE ------------------------------------------------------------ 10 | CREATE TABLE scrapelog ( 11 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 12 | `timeStarted` INT(11) NOT NULL, 13 | `timeEnded` INT(11) NOT NULL, 14 | `quartersAdded` TINYINT(3) UNSIGNED NOT NULL, 15 | `coursesAdded` INT UNSIGNED NOT NULL, 16 | `coursesUpdated` INT UNSIGNED NOT NULL, 17 | `sectionsAdded` INT UNSIGNED NOT NULL, 18 | `sectionsUpdated` INT UNSIGNED NOT NULL, 19 | `failures` INT UNSIGNED NOT NULL 20 | )ENGINE=InnoDB; -------------------------------------------------------------------------------- /schema/tables/sections.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- Sections Table 3 | -- 4 | -- @author Benjamin Russell (benrr101@csh.rit.edu) 5 | -- @descrip Table for storing all the sections and their information. They are 6 | -- also linked up with their parent course 7 | -- ------------------------------------------------------------------------- 8 | 9 | -- CREATE TABLE ------------------------------------------------------------ 10 | CREATE TABLE sections ( 11 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 12 | `course` INT UNSIGNED NOT NULL, 13 | `section` VARCHAR(4) NOT NULL, 14 | `title` VARCHAR(30) NOT NULL, 15 | `type` ENUM('R','N','OL','H', 'BL') NOT NULL DEFAULT 'R', 16 | `status` ENUM('O','C','X') NOT NULL, 17 | `instructor` VARCHAR(64) NOT NULL DEFAULT 'TBA', 18 | `maxenroll` SMALLINT(3) UNSIGNED NOT NULL, 19 | `curenroll` SMALLINT(3) UNSIGNED NOT NULL 20 | ) ENGINE=InnoDB; 21 | 22 | -- UNIQUE KEYS ------------------------------------------------------------- 23 | ALTER TABLE sections 24 | ADD CONSTRAINT UQ_sections_course_section 25 | UNIQUE (`course`, `section`); 26 | 27 | -- FOREIGN KEYS ------------------------------------------------------------ 28 | ALTER TABLE sections 29 | ADD CONSTRAINT FK_sections_course 30 | FOREIGN KEY (`course`) 31 | REFERENCES courses(`id`) 32 | ON UPDATE CASCADE 33 | ON DELETE CASCADE; -------------------------------------------------------------------------------- /schema/tables/times.sql: -------------------------------------------------------------------------------- 1 | -- ------------------------------------------------------------------------- 2 | -- Section Times Table 3 | -- 4 | -- @author Benjamin Russell (benrr101@csh.rit.edu) 5 | -- @descrip Table for storing the times that a section meets. Also includes 6 | -- foreign keys for linking up the times to the section and location 7 | -- to a building. 8 | -- ------------------------------------------------------------------------- 9 | 10 | -- CREATE TABLE ------------------------------------------------------------ 11 | CREATE TABLE times ( 12 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 13 | `section` INT UNSIGNED NOT NULL, 14 | `day` TINYINT(1) UNSIGNED NOT NULL, 15 | `start` SMALLINT(4) UNSIGNED NOT NULL, 16 | `end` SMALLINT(4) UNSIGNED NOT NULL, 17 | `building` VARCHAR(5) NULL DEFAULT NULL, 18 | `room` VARCHAR(10) NULL DEFAULT NULL 19 | )ENGINE=InnoDB; 20 | 21 | -- FOREIGN KEY CONSTRAINTS ------------------------------------------------- 22 | ALTER TABLE times 23 | ADD CONSTRAINT FK_times_section 24 | FOREIGN KEY (`section`) 25 | REFERENCES sections(`id`) 26 | ON UPDATE CASCADE 27 | ON DELETE CASCADE; 28 | 29 | ALTER TABLE times 30 | ADD CONSTRAINT FK_times_building 31 | FOREIGN KEY (`building`) 32 | REFERENCES buildings(`number`) 33 | ON DELETE SET NULL 34 | ON UPDATE CASCADE; -------------------------------------------------------------------------------- /tools/pruneSchedules.php: -------------------------------------------------------------------------------- 1 | (60 * 60 * 24 * 90))"; 21 | 22 | // Run the query to delete the courses of the schedule that are older than 23 | // 90 days 24 | $ninetyDaysAgo = date("Y-m-d H:i:s", strtotime("-90 days")); 25 | $query = "DELETE FROM schedules WHERE datelastaccessed < '{$ninetyDaysAgo}'"; 26 | $result = $dbConn->query($query); 27 | if(!$result) { 28 | echo("*** Failed to run pruning query:\n"); 29 | echo($dbConn->error . "\n"); 30 | } else { 31 | echo("... " . $dbConn->affected_rows . " schedules deleted\n"); 32 | } 33 | 34 | ?> 35 | -------------------------------------------------------------------------------- /tools/truncateCourses.sql: -------------------------------------------------------------------------------- 1 | TRUNCATE TABLE times; 2 | TRUNCATE TABLE courses; 3 | TRUNCATE TABLE sections; 4 | -------------------------------------------------------------------------------- /tools/truncateSchedules.sql: -------------------------------------------------------------------------------- 1 | TRUNCATE TABLE schedules; 2 | TRUNCATE TABLE schedulecourses; 3 | TRUNCATE TABLE schedulenoncourses; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./assets/src/modules/sm/**/*.ts"], 3 | "exclude": ["./node_modules"], 4 | "compilerOptions": { 5 | "module": "es2015", 6 | "moduleResolution": "node", 7 | "allowJs": false, 8 | "noImplicitAny": false, 9 | "target": "es5" 10 | } 11 | } 12 | --------------------------------------------------------------------------------