-
Notifications
You must be signed in to change notification settings - Fork 38
Expand file tree
/
Copy pathgenerate.php
More file actions
334 lines (279 loc) · 13.1 KB
/
generate.php
File metadata and controls
334 lines (279 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
<?php
/***************************************************************************
* GENERATION AJAX CALLS
* Provides standalone JSON object retrieval for schedule designing form and display
*
* PHP Version 7
*
* @author Ben Russell <benrr101@csh.rit.edu>
* @file api/generate.php
***************************************************************************/
// REQUIRED FILES
if (file_exists('../inc/config.php')) {
include_once "../inc/config.php";
} else {
include_once "../inc/config.env.php";
}
require_once "../inc/databaseConn.php";
require_once "../inc/timeFunctions.php";
require_once "../inc/ajaxError.php";
require_once "../api/src/Generate.php";
// IMPORTS
use API\Generate;
// GLOBALS /////////////////////////////////////////////////////////////////
$ERRORS = []; // Storage for course conflicts & recoverable errors
$generator = new Generate();
// ERROR HANDLING //////////////////////////////////////////////////////////
function ajaxErrorHandler($errno, $errstr, $errfile, $errline) {
echo(json_encode(["error" => "php", "msg" => $errstr, "num" => $errno, "file" => $errfile, "linenum" => $errline]));
die();
}
set_error_handler('ajaxErrorHandler');
// MAIN EXECUTION //////////////////////////////////////////////////////////
// We're providing JSON
header('Content-type: application/json');
// Escape the post data
$_POST = sanitize($_POST);
switch (getAction()) {
// GET COURSE OPTIONS //////////////////////////////////////////////////
case "getCourseOpts":
// @TODO: Move this over to the power search ajax handler
// Verify that we got a course (or partial course) and a quarter
if (empty($_POST['course'])) {
die(json_encode(["error" => "argument", "msg" => "You must provide at least one partial course number"]));
}
if (empty($_POST['term'])) {
die(json_encode(["error" => "argument", "msg" => "You must provide a term"]));
}
// Iterate over the multiple options
$courseOptions = [];
foreach (explode(',', $_POST['course']) as $course) {
// If the course has enough characters for a lab section but
// but doesn't match OR there are <= 12 characters but it isn't
// numeric, then they fucked up.
if (strlen($course) > 13) {
die(json_encode(["error" => "argument", "msg" => "Your courses must be in the format XXXX-XXX-XXLX"]));
}
$course = strtoupper($course);
preg_match('/([A-Z0-9]{4})[-\s]*(\d{0,3}[A-Z]?)?(?:[-\s]+(\d{0,2}[A-Z]?\d?))?/', $course, $courseParts);
// Query base: Noncancelled courses from the requested term
$query = "SELECT s.id
FROM courses AS c
JOIN sections AS s ON s.course = c.id
JOIN departments AS d ON c.department = d.id
WHERE
s.status != 'X'
AND c.quarter = '{$_POST['term']}'";
// Component 1: Department
$department = $courseParts[1];
if (strlen($department) != 4) {
// We didn't get an entire department. We won't proceed
die(json_encode(["error" => "argument", "msg" => "You must provide at least a complete department"]));
}
$query .= " AND (d.code = '{$department}' OR d.number = '{$department}')";
// Component 2: Course number
$coursenum = array_key_exists(2, $courseParts) ? $courseParts[2] : null;
if (!$coursenum || (strlen($coursenum) != 3 && strlen($coursenum) != 4)) {
// We got a partial course. That's ok.
$query .= " AND c.course LIKE '{$coursenum}%'";
} else {
// The user has specified a 3 or 4 character course number. If its 4 chars then the user had better know
// what they're doing.
$query .= " AND c.course = '{$coursenum}'";
}
// Component 3: Section number
$section = array_key_exists(3, $courseParts) ? $courseParts[3] : null;
if (!$section || strlen($coursenum) != 4) {
// We got a partial section number. That's ok.
$query .= " AND s.section LIKE '{$section}%'";
} else {
$query .= " AND s.section = '{$section}'";
}
// Ignore full courses option
if ($_POST['ignoreFull'] == 'true') {
$query .= " AND s.curEnroll < s.maxEnroll";
}
// Close it up and provide order
$query .= " ORDER BY c.course, s.section";
$result = $dbConn->query($query);
if (!$result) {
die(json_encode(["error" => "mysql", "msg" => "A database error occurred while searching for {$course}"]));
}
if ($result->num_rows == 0) {
continue;
}
// Fetch all the results and append them to the list
while ($row = $result->fetch_assoc()) {
$courseOptions[] = getCourseBySectionId($row['id']);
}
}
if (count($courseOptions) == 0) {
die(json_encode(["error" => "result", "msg" => "No courses match"]));
}
// Puke the results back to the user
echo json_encode($courseOptions);
break;
// GET MATCHING SCHEDULES //////////////////////////////////////////////
case "getMatchingSchedules":
// Process the list of courses that were selected
// Keep track of grouped classes by both clean course name (sections) and by course input index
/**
* array(string {cleanCourseNum} => array(string {courseNum} => {courseInfo array})))
*/
$courseGroups = [];
/**
* array(int {course input index} => array(integer {courseId} => array(string {courseNum} => {courseInfo array})))
*/
$courseGroupsByCourseId = [];
$courseSet = [];
// Check to make sure schedule wont exceed 10,000 options
$totalSchedules = 1;
for ($i = 1; $i <= $_POST['courseCount']; $i++) {
if (!isset($_POST["courses{$i}Opt"])) {
continue;
}
$totalSchedules *= count($_POST["courses{$i}Opt"]);
}
if ($totalSchedules >= 10000) {
echo json_encode([
"error" => "argument",
"msg" => "Too many schedule possibilities to generate, try to remove classes from your shopping cart.
Adding classes like YearOne or classes with hundreds of sections can cause this to occur.",
"arg" => "action"
]);
break;
}
for ($i = 1; $i <= $_POST['courseCount']; $i++) { // It's 1-indexed... :[
// Iterate over the courses in that course slot
if (!isset($_POST["courses{$i}Opt"])) {
continue;
}
$courseSubSet = [];
$courseGroupsByCourseId[$i] = [];
foreach ($_POST["courses{$i}Opt"] as $course) {
// Do a query to get the course specified
$courseInfo = getCourseBySectionId($course);
// courseIndex is only used by the frontend UI to determine what color/grouping to use
$courseInfo['courseIndex'] = $i;
// Remove the potential special indicators from the end of the courseNum
$cleanCourseNum = $generator->getCleanCourseNum($courseInfo);
// Create the group if it does not already exist
if (!array_key_exists($cleanCourseNum, $courseGroups)) {
$courseGroups[$cleanCourseNum] = [];
}
// Create the group by index and course id. Can probably ignore courseId, but will be eventually useful
if (!array_key_exists($courseInfo['courseId'], $courseGroupsByCourseId[$i])) {
$courseGroupsByCourseId[$i][$courseInfo['courseId']] = [];
}
// Check if the section is a special course: courseNum ending in a letter, then one or two digits
if (isSpecialSection($courseInfo)) {
if (!array_key_exists($courseInfo['courseNum'], $courseGroups[$cleanCourseNum])) {
// Add this course to its group
$courseGroups[$cleanCourseNum][$courseInfo['courseNum']] = $courseInfo;
}
if (!array_key_exists($courseInfo['courseNum'], $courseGroupsByCourseId[$i][$courseInfo['courseId']])) {
// Add this course to its group by course id
$courseGroupsByCourseId[$i][$courseInfo['courseId']][$courseInfo['courseNum']] = $courseInfo;
}
} else {
// This is a normal class, it can be added like normal to the sub set
$courseSubSet[] = $courseInfo;
}
}
// Add the normal subset to the main set
if (count($courseSubSet) > 0) {
$courseSet[] = $courseSubSet;
}
}
// Loop through each course groups' courses and flatten the array
if (count($courseGroups) > 0) {
foreach ($courseGroupsByCourseId as $courseGroupsByIndex) {
$specialCourseSubSet = [];
foreach ($courseGroupsByIndex as $courseGroup) {
// Get each special course
foreach ($courseGroup as $specialCourse) {
$specialCourseSubSet[] = $specialCourse;
}
}
// Add any special courses for this index to the main courseSet.
if (count($specialCourseSubSet) > 0) {
$courseSet[] = $specialCourseSubSet;
}
}
}
// Set the courseIndex for the remaining nonCourse/noCourse routines
$courseIndex = $i;
// Process the list of nonCourse Items
$nonCourseSet = [];
for ($i = 1; $i <= $_POST['nonCourseCount']; $i++) {
// If there are no days set for the item, ignore it
if (empty($_POST["nonCourseDays{$i}"])) {
continue;
}
// Create a new nonCourse Item
$nonCourse = [];
$nonCourse['title'] = $_POST["nonCourseTitle{$i}"];
$nonCourse['courseNum'] = "non";
$nonCourse['courseIndex'] = $courseIndex++;
$nonCourse['times'] = [];
// Create a time entry for each
foreach ($_POST["nonCourseDays{$i}"] as $day) {
$nonCourse['times'][] = [
"day" => translateDay($day),
"start" => intval($_POST["nonCourseStartTime{$i}"]),
"end" => intval($_POST["nonCourseEndTime{$i}"])
];
}
$nonCourseSet[] = $nonCourse;
}
// If both the nonCourse items AND the course items list is empty, we can't draw a schedule
if (empty($courseSet) && empty($nonCourseSet)) {
die(json_encode([
"error" => "user",
"msg" =>
"Cannot generate schedules because no courses or course items were provided"
]));
}
// Process the list of noCourse Times
$noCourseSet = [];
for ($i = 1; $i <= $_POST['noCourseCount']; $i++) {
// If there are no days set for the time slot, ignore it
if (empty($_POST["noCourseDays{$i}"])) {
continue;
}
// Create a new noCourse time slot
$noCourse = [];
$noCourse['times'] = [];
foreach ($_POST["noCourseDays{$i}"] as $day) {
$noCourse['times'][] = [
"day" => translateDay($day),
"start" => intval($_POST["noCourseStartTime{$i}"]),
"end" => intval($_POST["noCourseEndTime{$i}"])
];
}
$noCourseSet[] = $noCourse;
}
// Generate valid schedules, and include the errors if we're being verbose
$results = [];
if (!empty($courseSet)) {
$results['schedules'] = $generator->pruneSpecialCourses(
$generator->generateSchedules($courseSet, $nonCourseSet, $noCourseSet), $courseGroups);
} else {
$results['schedules'] = [[]];
}
// Add the nonCourse items to the schedules (they are guaranteed not to overlap via generateSchedules)
foreach ($results['schedules'] as $k => $schedule) {
foreach ($nonCourseSet as $nonCourse) {
$results['schedules'][$k][] = $nonCourse;
}
}
if (isset($_POST['verbose']) && $_POST['verbose'] && count($ERRORS)) {
$results['errors'] = $ERRORS;
}
echo json_encode($results);
break;
// DEFAULT ACTION //////////////////////////////////////////////////////
default:
echo json_encode(["error" => "argument", "msg" => "Invalid or no action provided", "arg" => "action"]);
break;
}