|
| 1 | +#!/usr/bin/env php |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * Generates a report of how many students have dropped courses. |
| 5 | + * |
| 6 | + * This is meant to be run immediately before and immediately after the autofeed. |
| 7 | + * The first run (before running the autofeed) will cache enrollment numbers in |
| 8 | + * a CSV temp file. The second run (after running the autofeed) will read the |
| 9 | + * cached results, compare with the database, compile the report, write to a |
| 10 | + * file, and optionally email it. The first CLI arg must be '1' on the first |
| 11 | + * run and '2' on the second run. Second CLI arg must be the term code. |
| 12 | + * e.g. $ ./add_drop_report.php 1 f21 will invoke first run for Fall 2021. |
| 13 | + * $ ./add_drop_report.php 2 f21 will invoke second run for Fall 2021. |
| 14 | + * |
| 15 | + * @author Peter Bailie, Renssealer Polytechnic Institute, Research Computing |
| 16 | + */ |
| 17 | + |
| 18 | +require "config.php"; |
| 19 | + |
| 20 | +if (php_sapi_name() !== "cli") { |
| 21 | + die("This is a command line script\n"); |
| 22 | +} |
| 23 | + |
| 24 | +if (!array_key_exists(1, $argv)) { |
| 25 | + die("Missing process pass # (1 or 2)"); |
| 26 | +} |
| 27 | + |
| 28 | +if (!array_key_exists(2, $argv)) { |
| 29 | + die("Missing term code.\n"); |
| 30 | +} |
| 31 | + |
| 32 | +$proc = new add_drop_report($argv); |
| 33 | +$proc->go(); |
| 34 | +exit; |
| 35 | + |
| 36 | +/** |
| 37 | + * Main process class |
| 38 | + * |
| 39 | + * @param array $argv |
| 40 | + */ |
| 41 | +class add_drop_report { |
| 42 | + |
| 43 | + /** @var string "Pass" as in which pass is being run: "1" or "2" */ |
| 44 | + private $pass; |
| 45 | + |
| 46 | + /** @var string academic term / semester code. e.g. "f21" for Fall 2021 */ |
| 47 | + private $term; |
| 48 | + |
| 49 | + public function __construct($argv) { |
| 50 | + $this->pass = $argv[1]; |
| 51 | + $this->term = $argv[2]; |
| 52 | + } |
| 53 | + |
| 54 | + public function __destruct() { |
| 55 | + db::close(); |
| 56 | + } |
| 57 | + |
| 58 | + /** Main process flow |
| 59 | + * |
| 60 | + * $argv[1] = "1": First run to read course list and cache results to CSV temp file |
| 61 | + * $argv[1] = "2": Second run to compare cached results with database and make report\ |
| 62 | + */ |
| 63 | + public function go() { |
| 64 | + switch($this->pass) { |
| 65 | + case "1": |
| 66 | + // Record current course enrollments to temporary CSV |
| 67 | + db::open(); |
| 68 | + $courses = db::get_courses($this->term); |
| 69 | + $mapped_courses = db::get_mapped_courses($this->term); |
| 70 | + $enrollments = db::count_enrollments($this->term, $courses, $mapped_courses); |
| 71 | + $course_enrollments = $enrollments[0]; |
| 72 | + // ----------------------------------------------------------------- |
| 73 | + reports::write_temp_csv($course_enrollments); |
| 74 | + return null; |
| 75 | + case "2": |
| 76 | + // Read temporary CSV and compile and send add/drop report. |
| 77 | + db::open(); |
| 78 | + $courses = db::get_courses($this->term); |
| 79 | + $mapped_courses = db::get_mapped_courses($this->term); |
| 80 | + $enrollments = db::count_enrollments($this->term, $courses, $mapped_courses); |
| 81 | + $course_enrollments = $enrollments[0]; |
| 82 | + $manual_flags = $enrollments[1]; |
| 83 | + // ----------------------------------------------------------------- |
| 84 | + $prev_course_enrollments = reports::read_temp_csv(); |
| 85 | + $report = reports::compile_report($prev_course_enrollments, $course_enrollments, $manual_flags); |
| 86 | + reports::send_report($this->term, $report); |
| 87 | + return null; |
| 88 | + default: |
| 89 | + die("Unrecognized pass \"{$this->pass}\"\n"); |
| 90 | + } |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +/** Static callback functions used with array_walk() */ |
| 95 | +class callbacks { |
| 96 | + /** Convert string to lowercase */ |
| 97 | + public static function strtolower_cb(&$val, $key) { $val = strtolower($val); } |
| 98 | + |
| 99 | + /** Convert array to CSV data (as string) */ |
| 100 | + public static function str_getcsv_cb(&$val, $key) { $val = str_getcsv($val, CSV_DELIM_CHAR); } |
| 101 | +} |
| 102 | + |
| 103 | +/** Database static class */ |
| 104 | +class db { |
| 105 | + /** @var resource DB connection resource */ |
| 106 | + private static $db = null; |
| 107 | + |
| 108 | + /** Open connection to DB */ |
| 109 | + public static function open() { |
| 110 | + // constants defined in config.php |
| 111 | + $user = DB_LOGIN; |
| 112 | + $host = DB_HOST; |
| 113 | + $password = DB_PASSWORD; |
| 114 | + |
| 115 | + self::$db = pg_connect("host={$host} dbname=submitty user={$user} password={$password} sslmode=prefer"); |
| 116 | + if (!self::check()) { |
| 117 | + die("Failed to connect to DB\n"); |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + /** Close connection to DB */ |
| 122 | + public static function close() { |
| 123 | + if (self::check()) { |
| 124 | + pg_close(self::$db); |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + /** |
| 129 | + * Verify that DB connection resource is OK |
| 130 | + * |
| 131 | + * @access private |
| 132 | + * @return bool true when DB connection resource is OK, false otherwise. |
| 133 | + */ |
| 134 | + private static function check() { |
| 135 | + return is_resource(self::$db) && pg_connection_status(self::$db) === PGSQL_CONNECTION_OK; |
| 136 | + } |
| 137 | + |
| 138 | + /** |
| 139 | + * Retrieve course list from DB's courses table |
| 140 | + * |
| 141 | + * @param string $term |
| 142 | + * @return string[] |
| 143 | + */ |
| 144 | + public static function get_courses($term) { |
| 145 | + if (!self::check()) { |
| 146 | + die("Not connected to DB when querying course list\n"); |
| 147 | + } |
| 148 | + |
| 149 | + // Undergraduate courses from DB. |
| 150 | + $sql = "SELECT course FROM courses WHERE semester=$1 AND status=1"; |
| 151 | + $params = array($term); |
| 152 | + $res = pg_query_params(self::$db, $sql, $params); |
| 153 | + if ($res === false) |
| 154 | + die("Failed to retrieve course list from DB\n"); |
| 155 | + $course_list = pg_fetch_all_columns($res, 0); |
| 156 | + array_walk($course_list, 'callbacks::strtolower_cb'); |
| 157 | + |
| 158 | + return $course_list; |
| 159 | + } |
| 160 | + |
| 161 | + /** |
| 162 | + * Retrieve mapped courses from DB's mapped_courses table |
| 163 | + * |
| 164 | + * @param $term |
| 165 | + * @return string[] [course] => mapped_course |
| 166 | + */ |
| 167 | + public static function get_mapped_courses($term) { |
| 168 | + if (!self::check()) { |
| 169 | + die("Not connected to DB when querying mapped courses list\n"); |
| 170 | + } |
| 171 | + |
| 172 | + // mapped courses from DB |
| 173 | + $sql = "SELECT course, mapped_course FROM mapped_courses WHERE semester=$1"; |
| 174 | + $params = array($term); |
| 175 | + $res = pg_query_params(self::$db, $sql, $params); |
| 176 | + if ($res === false) { |
| 177 | + die("Failed to retrieve mapped courses from DB\n"); |
| 178 | + } |
| 179 | + |
| 180 | + $keys = pg_fetch_all_columns($res, 0); |
| 181 | + array_walk($keys, 'callbacks::strtolower_cb'); |
| 182 | + $vals = pg_fetch_all_columns($res, 1); |
| 183 | + array_walk($vals, 'callbacks::strtolower_cb'); |
| 184 | + $mapped_courses = array_combine($keys, $vals); |
| 185 | + |
| 186 | + return $mapped_courses; |
| 187 | + } |
| 188 | + |
| 189 | + /** |
| 190 | + * Retrieve number of students (1) with manual flag set, (2) enrolled in courses |
| 191 | + * |
| 192 | + * @param $term |
| 193 | + * @param $course_list |
| 194 | + * @param $mapped_courses |
| 195 | + * @return int[] ([0] => course enrollment counts, [1] => manual flag counts) |
| 196 | + */ |
| 197 | + public static function count_enrollments($term, $course_list, $mapped_courses) { |
| 198 | + if (!self::check()) { |
| 199 | + die("Not connected to DB when querying course enrollments\n"); |
| 200 | + } |
| 201 | + |
| 202 | + $course_enrollments = array(); |
| 203 | + $manual_flags = array(); |
| 204 | + |
| 205 | + foreach ($course_list as $course) { |
| 206 | + $grad_course = array_search($course, $mapped_courses); |
| 207 | + if ($grad_course === false) { |
| 208 | + // COURSE HAS NO GRAD SECTION (not mapped). |
| 209 | + $sql = "SELECT COUNT(*) FROM courses_users WHERE semester=$1 AND course=$2 AND user_group=4 AND registration_section IS NOT NULL"; |
| 210 | + $params = array($term, $course); |
| 211 | + $res = pg_query_params(self::$db, $sql, $params); |
| 212 | + if ($res === false) |
| 213 | + die("Failed to lookup enrollments for {$course}\n"); |
| 214 | + $course_enrollments[$course] = (int) pg_fetch_result($res, 0); |
| 215 | + |
| 216 | + // Get manual flag count |
| 217 | + $sql = "SELECT COUNT(*) FROM courses_users WHERE semester=$1 AND course=$2 AND user_group=4 AND registration_section IS NOT NULL AND manual_registration=TRUE"; |
| 218 | + $res = pg_query_params(self::$db, $sql, $params); |
| 219 | + if ($res === false) |
| 220 | + die("Failed to lookup counts with manual flag set for {$course}\n"); |
| 221 | + $manual_flags[$course] = (int) pg_fetch_result($res, 0); |
| 222 | + } else { |
| 223 | + // UNDERGRADUATE SECTION |
| 224 | + $sql = "SELECT COUNT(*) FROM courses_users WHERE semester=$1 AND course=$2 AND user_group=4 AND registration_section='1'"; |
| 225 | + $params = array($term, $course); |
| 226 | + $res = pg_query_params(self::$db, $sql, $params); |
| 227 | + if ($res === false) |
| 228 | + die("Failed to lookup enrollments for {$course}\n"); |
| 229 | + $course_enrollments[$course] = (int) pg_fetch_result($res, 0); |
| 230 | + |
| 231 | + // Get manual flag count |
| 232 | + $sql = "SELECT COUNT(*) FROM courses_users WHERE semester=$1 AND course=$2 AND user_group=4 AND registration_section='1' AND manual_registration=TRUE"; |
| 233 | + $res = pg_query_params(self::$db, $sql, $params); |
| 234 | + if ($res === false) |
| 235 | + die("Failed to lookup counts with manual flag set for {$course} (undergrads)\n"); |
| 236 | + $manual_flags[$course] = (int) pg_fetch_result($res, 0); |
| 237 | + |
| 238 | + // GRADUATE SECTION |
| 239 | + $sql = "SELECT COUNT(*) FROM courses_users WHERE semester=$1 AND course=$2 AND user_group=4 AND registration_section='2'"; |
| 240 | + $res = pg_query_params(self::$db, $sql, $params); |
| 241 | + if ($res === false) |
| 242 | + die("Failed to lookup enrollments for {$grad_course}\n"); |
| 243 | + $course_enrollments[$grad_course] = (int) pg_fetch_result($res, 0); |
| 244 | + |
| 245 | + // Get manual flag count |
| 246 | + $sql = "SELECT COUNT(*) FROM courses_users WHERE semester=$1 AND course=$2 AND user_group=4 AND registration_section='2' AND manual_registration=TRUE"; |
| 247 | + $res = pg_query_params(self::$db, $sql, $params); |
| 248 | + if ($res === false) |
| 249 | + die("Failed to lookup counts with manual flag set for {$course} (grads)\n"); |
| 250 | + $manual_flags[$grad_course] = (int) pg_fetch_result($res, 0); |
| 251 | + } |
| 252 | + } |
| 253 | + |
| 254 | + // Courses make up array keys. Sort by courses. |
| 255 | + ksort($course_enrollments); |
| 256 | + ksort($manual_flags); |
| 257 | + return array($course_enrollments, $manual_flags); |
| 258 | + } |
| 259 | +} |
| 260 | + |
| 261 | +/** Reports related methods */ |
| 262 | +class reports { |
| 263 | + /** |
| 264 | + * Write course enrollment counts to temporary CSV file |
| 265 | + * |
| 266 | + * @param $course_enrollments |
| 267 | + */ |
| 268 | + public static function write_temp_csv($course_enrollments) { |
| 269 | + $today = date("ymd"); |
| 270 | + $tmp_path = ADD_DROP_FILES_PATH . "tmp/"; |
| 271 | + $tmp_file = "{$today}.tmp"; |
| 272 | + |
| 273 | + if (!is_dir($tmp_path)) { |
| 274 | + if (!mkdir($tmp_path, 0770, true)) { |
| 275 | + die("Can't create tmp folder.\n"); |
| 276 | + } |
| 277 | + } |
| 278 | + |
| 279 | + $fh = fopen($tmp_path . $tmp_file, "w"); |
| 280 | + if ($fh === false) { |
| 281 | + die("Could not create temp file.\n"); |
| 282 | + } |
| 283 | + |
| 284 | + foreach($course_enrollments as $course=>$num_students) { |
| 285 | + fputcsv($fh, array($course, $num_students), CSV_DELIM_CHAR); |
| 286 | + } |
| 287 | + fclose($fh); |
| 288 | + chmod($tmp_path . $tmp_file, 0660); |
| 289 | + } |
| 290 | + |
| 291 | + /** |
| 292 | + * Read temporary CSV file. Delete it when done. |
| 293 | + * |
| 294 | + * @return string[] "previous" course list of [course] => num_students |
| 295 | + */ |
| 296 | + public static function read_temp_csv() { |
| 297 | + $today = date("ymd"); |
| 298 | + $tmp_path = ADD_DROP_FILES_PATH . "tmp/"; |
| 299 | + $tmp_file = "{$today}.tmp"; |
| 300 | + |
| 301 | + $csv = file($tmp_path . $tmp_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); |
| 302 | + if ($csv === false) { |
| 303 | + die("Could not read temp file to prepare report.\n"); |
| 304 | + } |
| 305 | + |
| 306 | + unlink($tmp_path . $tmp_file); // remove tmp file. |
| 307 | + array_walk($csv, 'callbacks::str_getcsv_cb'); |
| 308 | + // return array of array('course' => enrollment). e.g. ('csci1000' => 100) |
| 309 | + return array_combine(array_column($csv, 0), array_column($csv, 1)); |
| 310 | + } |
| 311 | + |
| 312 | + /** |
| 313 | + * Compile $report from params' data |
| 314 | + * |
| 315 | + * @param $prev_course_enrollments |
| 316 | + * @param $course_enrollments |
| 317 | + * @param $manual_flags |
| 318 | + * @return string $report |
| 319 | + */ |
| 320 | + public static function compile_report($prev_course_enrollments, $course_enrollments, $manual_flags) { |
| 321 | + // Compile stats |
| 322 | + $date = date("F j, Y"); |
| 323 | + $time = date("g:i A"); |
| 324 | + $report = "Student autofeed counts report for {$date} at {$time}\n"; |
| 325 | + $report .= "NOTE: Difference and ratio do not account for the manual flag.\n"; |
| 326 | + $report .= "COURSE YESTERDAY TODAY MANUAL DIFFERENCE RATIO\n"; |
| 327 | + |
| 328 | + foreach ($course_enrollments as $course=>$course_enrollment) { |
| 329 | + // Calculate data |
| 330 | + $prev_course_enrollment = array_key_exists($course, $prev_course_enrollments) ? $prev_course_enrollments[$course] : 0; |
| 331 | + $manual_flag = array_key_exists($course, $manual_flags) ? $manual_flags[$course] : 0; |
| 332 | + $diff = $course_enrollment - $prev_course_enrollment; |
| 333 | + $ratio = $prev_course_enrollment != 0 ? abs(round(($diff / $prev_course_enrollment), 3)) : "N/A"; |
| 334 | + |
| 335 | + // Align into columns |
| 336 | + $course = str_pad($course, 18, " ", STR_PAD_RIGHT); |
| 337 | + $prev_course_enrollment = str_pad($prev_course_enrollment, 5, " ", STR_PAD_LEFT); |
| 338 | + $course_enrollment = str_pad($course_enrollment, 5, " ", STR_PAD_LEFT); |
| 339 | + $manual_flag = str_pad($manual_flag, 6, " ", STR_PAD_LEFT); |
| 340 | + $diff = str_pad($diff, 10, " ", STR_PAD_LEFT); |
| 341 | + |
| 342 | + // Add row to report. |
| 343 | + $report .= "{$course}{$prev_course_enrollment} {$course_enrollment} {$manual_flag} {$diff} {$ratio}\n"; |
| 344 | + } |
| 345 | + |
| 346 | + return $report; |
| 347 | + } |
| 348 | + |
| 349 | + /** |
| 350 | + * Write $report to file. Optionally send $report by email. |
| 351 | + * |
| 352 | + * Email requires sendmail (or equivalent) installed and configured in php.ini. |
| 353 | + * Emails are sent "unauthenticated". |
| 354 | + * |
| 355 | + * @param $term |
| 356 | + * @param $repprt |
| 357 | + */ |
| 358 | + public static function send_report($term, $report) { |
| 359 | + // Email stats (print stats if email is null or otherwise not sent) |
| 360 | + if (!is_null(ADD_DROP_TO_EMAIL)) { |
| 361 | + $date = date("M j, Y"); |
| 362 | + $to = ADD_DROP_TO_EMAIL; |
| 363 | + $from = ADD_DROP_FROM_EMAIL; |
| 364 | + $subject = "Submitty Autofeed Add/Drop Report For {$date}"; |
| 365 | + $report = str_replace("\n", "\r\n", $report); // needed for email formatting |
| 366 | + $is_sent = mail($to, $subject, $report, array('from' => $from)); |
| 367 | + if (!$is_sent) { |
| 368 | + $report = str_replace("\r\n", "\n", $report); // revert back since not being emailed. |
| 369 | + fprintf(STDERR, "Add/Drop report could not be emailed.\n%s", $report); |
| 370 | + } |
| 371 | + } |
| 372 | + |
| 373 | + // Write report to file. |
| 374 | + $path = ADD_DROP_FILES_PATH . $term . "/"; |
| 375 | + if (!is_dir($path)) { |
| 376 | + if (!mkdir($path, 0770, true)) { |
| 377 | + die("Cannot create reports path {$path}.\n"); |
| 378 | + } |
| 379 | + } |
| 380 | + |
| 381 | + $today = date("Y-m-d"); |
| 382 | + file_put_contents("{$path}report_{$today}.txt", $report); |
| 383 | + chmod("{$path}report_{$today}.txt", 0660); |
| 384 | + } |
| 385 | +} |
| 386 | + |
| 387 | +// EOF |
| 388 | +?> |
0 commit comments