"
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 | 
7 | 
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: 'Previous ' +
10 | ' {{displayOptions.currentPage+1}}/{{numberOfPages()}} ' +
11 | 'Next ',
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 `
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 |
4 |
--------------------------------------------------------------------------------
/assets/src/modules/sm/App/templates/cart.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 | Course Cart
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
{{school | codeOrNumber}}
37 |
{{school.title?school.title:" "}}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
{{department.code?department.code:department.number}}
47 |
{{department.title?department.title:" "}}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
{{course | courseNum}}
57 |
{{course.title}}
58 |
59 |
60 |
Toggle Desciption
61 |
{{course.description}}
62 |
63 |
64 |
65 | {{course.selected ? 'Remove all':'Add all'}}
66 |
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 |
91 |
92 |
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 |
121 | Reset...
122 |
123 |
126 |
127 |
128 |
129 |
130 |
131 | Reset...
132 |
133 |
136 |
137 |
138 |
139 |
140 |
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 |
15 |
16 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
19 |
27 |
35 |
36 |
37 | Print
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/assets/src/modules/sm/Schedule/templates/schedule.view.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 | Scrape Started
12 | Scrape Finished
13 | Time Elapsed
14 | Courses Added
15 | Courses Updated
16 | Sections Added
17 | Sections Updated
18 | Failures
19 |
20 |
21 |
22 | No Logs Exist
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
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 = "";
39 | foreach ($days as $dayCode => $dayName) {
40 | $result .= "{$dayName} ";
41 | }
42 | $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 = "";
67 | foreach ($times as $time) {
68 | $result .= "" . translateTime($time,
69 | $twelve) . " ";
70 | }
71 | $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 | if(!empty($IMGURL)) { ?>
71 |
72 | } else { ?>
73 |
74 | } ?>
75 |
76 |
77 |
78 |
98 |
101 |
102 |
103 |
104 |
Version: =$APP_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 |
116 | Made Using CSH ScheduleMaker
117 |
118 |
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 |
--------------------------------------------------------------------------------