Skip to content

Commit 0071454

Browse files
authored
Autofeed v2 (#22)
* Student Autofeed Updates * Student Auto Feed Update * Update readme.md * Update readme.md * Student Enrollment Report * Validate drop ratio Now checks dropped student ratio and blocks upsert if the ratio is beyond a cutoff value set in config.php * Bugfix Fix crash bug when there are no mapped courses. * Bugfix Can now run properly from any directory using full execution path.
1 parent 8b65cac commit 0071454

8 files changed

+1312
-771
lines changed

student_auto_feed/add_drop_report.php

+388
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
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

Comments
 (0)