Skip to content

Commit d3d5bd3

Browse files
authored
[Feature:System] RCOS Custom Mapping (#38)
Per internal RPI request from @bmcutler and Prof. Wes Turner Because registration source data no longer differentiates RCOS credit load by CRN, a custom section mapping is implemented in the student auto feed. RCOS course enrollments, per each student, will now be mapped to `{course}-{credits}`. e.g. A student enrolled in CSCI4700 for 4 credits will show in RCOS registration section `CSCI4700-4`.
1 parent 31710c5 commit d3d5bd3

File tree

5 files changed

+110
-90
lines changed

5 files changed

+110
-90
lines changed

student_auto_feed/config.php

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
/* HEADING ---------------------------------------------------------------------
44
*
55
* config.php script used by submitty_student_auto_feed
6-
* By Peter Bailie, Systems Programmer (RPI dept of computer science)
7-
*
8-
* Requires minimum PHP version 7.3 with pgsql extension.
6+
* By Peter Bailie, Renssealer Polytechnic Institute
97
*
108
* Configuration of submitty_student_auto_feed is structured through a series
119
* of named constants.
@@ -117,6 +115,7 @@
117115
define('COLUMN_EMAIL', 4); //Student's Campus Email
118116
define('COLUMN_TERM_CODE', 11); //Semester code used in data validation
119117
define('COLUMN_REG_ID', 12); //Course and Section registration ID
118+
define('COLUMN_CREDITS', 13); //Credits registered
120119

121120
//Validate term code. Set to null to disable this check.
122121
define('EXPECTED_TERM_CODE', '201705');
@@ -127,6 +126,25 @@
127126
//Set to true, if Submitty is using SAML for authentication.
128127
define('PROCESS_SAML', true);
129128

129+
/* RENSSELAER CENTER FOR OPEN SOURCE (RCOS) -----------------------------------
130+
* RCOS is not just one course, but several. Some of these courses also
131+
* permit a student to declare their credit load. The data feed will need
132+
* a column showing a student's credit load. See above: COLUMN_CREDITS
133+
*
134+
* Create only one RCOS course in Submitty, which will show up in the
135+
* grader's/instructor's course list. The other RCOS courses must be mapped to
136+
* this first course. Registration sections do need to be fully mapped, as the
137+
* database does not permit mapping NULL sections. However, the upsert process
138+
* will override how RCOS enrollments are translated, so that registration
139+
* sections are, per student, "{course}-{credits}" e.g. J. Doe is enrolled in
140+
* RCOS course CSCI4700 for 4 credits. They will be listed as enrolled in
141+
* registration section "CSCI4700-4"
142+
*/
143+
144+
// List *ALL* RCOS courses, as an array.
145+
// If you are not tracking RCOS, then set this as null or an empty array.
146+
define('RCOS_COURSE_LIST', null);
147+
130148
/* DATA SOURCING --------------------------------------------------------------
131149
* The Student Autofeed provides helper scripts to retrieve the CSV file for
132150
* processing. Shell script ssaf.sh is used to invoke one of the helper

student_auto_feed/readme.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ policies and practices.__
1010

1111
Detailed instructions can be found at [http://submitty.org/sysadmin/student\_auto\_feed](http://submitty.org/sysadmin/student_auto_feed)
1212

13-
Requirements: PHP 7.3 or higher with pgsql extension. `imap_remote.php` also
14-
requires the imap extension. This system is intended to be platform agnostic,
15-
but has been developed and tested with Ubuntu Linux.
13+
Requires the pgsql extension. `imap_remote.php` also requires the imap extension.
14+
This system is intended to be platform agnostic, but has been developed and tested
15+
with Ubuntu Linux.
1616

1717
## submitty\_student\_auto\_feed.php
1818
A command line executable script to read a student enrollment data CSV file and

student_auto_feed/ssaf_rcos.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
namespace ssaf;
3+
4+
/**
5+
* Static utilty class to support RCOS (Rensselaer Center for Open Source)
6+
*
7+
* This will override enrollment registration sections for RCOS courses to `{course}-{credits}` e.g. `CSCI4700-4`.
8+
* Some RCOS students may declare how many credits they are registering for, so normal course mapping in the database
9+
* is insufficient. This must done while processing the CSV because every override requires each student's registered
10+
* credits from the CSV.
11+
*
12+
* @author Peter Bailie
13+
*/
14+
class rcos {
15+
private array $course_list;
16+
17+
public function __construct() {
18+
$this->course_list = RCOS_COURSE_LIST ?? [];
19+
array_walk($this->course_list, function(&$v, $i) { $v = strtolower($v); });
20+
sort($this->course_list, SORT_STRING);
21+
}
22+
23+
/** Adjusts `$row[COLUMN_SECTION]` when `$course` is an RCOS course. */
24+
public function map(string $course, array &$row): void {
25+
if (in_array($course, $this->course_list, true)) {
26+
$course = strtoupper($course);
27+
$row[COLUMN_SECTION] = "{$course}-{$row[COLUMN_CREDITS]}";
28+
}
29+
}
30+
}

student_auto_feed/ssaf_validate.php

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -74,48 +74,6 @@ public static function validate_row($row, $row_num) : bool {
7474
return true;
7575
}
7676

77-
/**
78-
* Check $rows for duplicate user IDs.
79-
*
80-
* Submitty's master DB does not permit students to register more than once
81-
* for any course. It would trigger a key violation exception. This
82-
* function checks for data anomalies where a student shows up in a course
83-
* more than once as that is indicative of an issue with CSV file data.
84-
* Returns TRUE, as in no error, when $rows has all unique user IDs.
85-
* False, as in error found, otherwise. $user_ids is filled when return
86-
* is FALSE.
87-
*
88-
* @param array $rows Data rows to check (presumably an entire couse).
89-
* @param string[] &$user_id Duplicated user ID, when found.
90-
* @param string[] &$d_rows Rows containing duplicate user IDs, indexed by user ID.
91-
* @return bool TRUE when all user IDs are unique, FALSE otherwise.
92-
*/
93-
public static function check_for_duplicate_user_ids(array $rows, &$user_ids, &$d_rows) : bool {
94-
usort($rows, function($a, $b) { return $a[COLUMN_USER_ID] <=> $b[COLUMN_USER_ID]; });
95-
96-
$user_ids = [];
97-
$d_rows = [];
98-
$are_all_unique = true; // Unless proven FALSE
99-
$length = count($rows);
100-
for ($i = 1; $i < $length; $i++) {
101-
$j = $i - 1;
102-
if ($rows[$i][COLUMN_USER_ID] === $rows[$j][COLUMN_USER_ID]) {
103-
$are_all_unique = false;
104-
$user_id = $rows[$i][COLUMN_USER_ID];
105-
$user_ids[] = $user_id;
106-
$d_rows[$user_id][] = $j;
107-
$d_rows[$user_id][] = $i;
108-
}
109-
}
110-
111-
foreach($d_rows as &$d_row) {
112-
array_unique($d_row, SORT_REGULAR);
113-
}
114-
unset($d_row);
115-
116-
return $are_all_unique;
117-
}
118-
11977
/**
12078
* Validate that there isn't an excessive drop ratio in course enrollments.
12179
*

student_auto_feed/submitty_student_auto_feed.php

Lines changed: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*
66
* This script will read a student enrollment CSV feed provided by the campus
77
* registrar or data warehouse and "upsert" (insert/update) the feed into
8-
* Submitty's course databases. Requires PHP 7.3 and pgsql extension.
8+
* Submitty's course databases. Requires pgsql extension.
99
*
1010
* @author Peter Bailie, Rensselaer Polytechnic Institute
1111
*/
@@ -15,6 +15,7 @@
1515
require __DIR__ . "/ssaf_cli.php";
1616
require __DIR__ . "/ssaf_db.php";
1717
require __DIR__ . "/ssaf_validate.php";
18+
require __DIR__ . "/ssaf_rcos.php";
1819

1920
// Important: Make sure we are running from CLI
2021
if (php_sapi_name() !== "cli") {
@@ -27,22 +28,24 @@
2728

2829
/** primary process class */
2930
class submitty_student_auto_feed {
30-
/** @var resource File handle to read CSV */
31+
/** File handle to read CSV */
3132
private $fh;
32-
/** @var string Semester code */
33-
private $semester;
34-
/** @var array List of courses registered in Submitty */
35-
private $course_list;
36-
/** @var array Describes how courses are mapped from one to another */
37-
private $mapped_courses;
38-
/** @var array Describes courses/sections that are duplicated to other courses/sections */
39-
private $crn_copymap;
40-
/** @var array Courses with invalid data. */
41-
private $invalid_courses;
42-
/** @var array All CSV data to be upserted */
43-
private $data;
44-
/** @var string Ongoing string of messages to write to logfile */
45-
private $log_msg_queue;
33+
/** Semester code */
34+
private string $semester;
35+
/** List of courses registered in Submitty */
36+
private array $course_list;
37+
/** Describes how courses are mapped from one to another */
38+
private array $mapped_courses;
39+
/** Describes courses/sections that are duplicated to other courses/sections */
40+
private array $crn_copymap;
41+
/** Courses with invalid data. */
42+
private array $invalid_courses;
43+
/** All CSV data to be upserted */
44+
private array $data;
45+
/** Ongoing string of messages to write to logfile */
46+
private string $log_msg_queue;
47+
/** For special cases involving Renssealer Center for Open Source */
48+
private object $rcos;
4649

4750
/** Init properties. Open DB connection. Open CSV file. */
4851
public function __construct() {
@@ -100,6 +103,9 @@ public function __construct() {
100103
// Get CRN shared courses/sections (when a course/section is copied to another course/section)
101104
$this->crn_copymap = $this->read_crn_copymap();
102105

106+
// Helper object for special-cases involving RCOS.
107+
$this->rcos = new rcos();
108+
103109
// Init other properties.
104110
$this->invalid_courses = [];
105111
$this->data = [];
@@ -135,8 +141,8 @@ public function go() {
135141
case $this->check_for_excessive_dropped_users():
136142
// This check will block all upserts when an error is detected.
137143
exit(1);
138-
case $this->check_for_duplicate_user_ids():
139-
$this->log_it("Duplicate user IDs detected in CSV file.");
144+
case $this->filter_duplicate_registrations():
145+
// Never returns false. Error messages are already in log queue.
140146
break;
141147
case $this->invalidate_courses():
142148
// Should do nothing when $this->invalid_courses is empty
@@ -185,15 +191,15 @@ private function get_csv_data() {
185191
// Read and assign csv rows into $this->data array
186192
$row = fgetcsv($this->fh, 0, CSV_DELIM_CHAR);
187193
while(!feof($this->fh)) {
188-
// Course is comprised of an alphabetic prefix and a numeric suffix.
189-
$course = strtolower($row[COLUMN_COURSE_PREFIX] . $row[COLUMN_COURSE_NUMBER]);
190-
191194
// Trim whitespace from all fields in $row.
192195
array_walk($row, function(&$val, $key) { $val = trim($val); });
193196

194197
// Remove any leading zeroes from "integer" registration sections.
195198
if (ctype_digit($row[COLUMN_SECTION])) $row[COLUMN_SECTION] = ltrim($row[COLUMN_SECTION], "0");
196199

200+
// Course is comprised of an alphabetic prefix and a numeric suffix.
201+
$course = strtolower($row[COLUMN_COURSE_PREFIX] . $row[COLUMN_COURSE_NUMBER]);
202+
197203
switch(true) {
198204
// Check that $row has an appropriate student registration.
199205
case array_search($row[COLUMN_REGISTRATION], $all_valid_reg_codes) === false:
@@ -212,6 +218,9 @@ private function get_csv_data() {
212218
// Check that $row is associated with the course list.
213219
case array_search($course, $this->course_list) !== false:
214220
if (validate::validate_row($row, $row_num)) {
221+
// Check (and perform) special-case RCOS registration section mapping.
222+
$this->rcos->map($course, $row);
223+
215224
// Include $row
216225
$this->data[$course][] = $row;
217226

@@ -233,8 +242,13 @@ private function get_csv_data() {
233242
if (array_key_exists($section, $this->mapped_courses[$course])) {
234243
$m_course = $this->mapped_courses[$course][$section]['mapped_course'];
235244
if (validate::validate_row($row, $row_num)) {
236-
// Include $row.
245+
// Do course mapping (alters registration section).
237246
$row[COLUMN_SECTION] = $this->mapped_courses[$course][$section]['mapped_section'];
247+
248+
// Check (and override) for special-case RCOS registration section mapping.
249+
$this->rcos->map($course, $row);
250+
251+
// Include $row.
238252
$this->data[$m_course][] = $row;
239253

240254
// $row with a blank email is allowed, but it is also logged.
@@ -285,31 +299,31 @@ private function get_csv_data() {
285299
}
286300

287301
/**
288-
* Users cannot be registered to the same course multiple times.
302+
* Students cannot be registered to the same course multiple times.
289303
*
290-
* Any course with a user registered more than once is flagged invalid as
291-
* it is indicative of data errors from the CSV file.
292-
*
293-
* @return bool always TRUE
304+
* If multiple registrations for the same student and course are found, the first instance is allowed to be
305+
* upserted to the database. All other instances are removed from the data set and therefore not upserted.
294306
*/
295-
private function check_for_duplicate_user_ids() {
296-
foreach($this->data as $course => $rows) {
297-
$user_ids = null;
298-
$d_rows = null;
299-
// Returns FALSE (as in there is an error) when duplicate IDs are found.
300-
// However, a duplicate ID does not invalidate a course. Instead, the
301-
// first enrollment is accepted, the other enrollments are discarded,
302-
// and the event is logged.
303-
if (validate::check_for_duplicate_user_ids($rows, $user_ids, $d_rows) === false) {
304-
foreach($d_rows as $user_id => $userid_rows) {
305-
$length = count($userid_rows);
306-
for ($i = 1; $i < $length; $i++) {
307-
unset($this->data[$course][$userid_rows[$i]]);
308-
}
307+
private function filter_duplicate_registrations(): true {
308+
foreach($this->data as $course => &$rows) {
309+
usort($rows, function($a, $b) { return $a[COLUMN_USER_ID] <=> $b[COLUMN_USER_ID]; });
310+
$duplicated_ids = [];
311+
$num_rows = count($rows);
312+
313+
// We are iterating from bottom to top through a course's data set. Should we find a duplicate registration
314+
// and unset it from the array, (1) we are unsetting duplicates starting from the bottom, (2) which preserves
315+
// the first entry among duplicate entries, and (3) we do not make a comparison with a null key.
316+
for ($j = $num_rows - 1, $i = $j - 1; $i >= 0; $i--, $j--) {
317+
if ($rows[$i][COLUMN_USER_ID] === $rows[$j][COLUMN_USER_ID]) {
318+
$duplicated_ids[] = $rows[$j][COLUMN_USER_ID];
319+
unset($rows[$j]);
309320
}
321+
}
310322

323+
if (count($duplicated_ids) > 0) {
324+
array_unique($duplicated_ids, SORT_STRING);
311325
$msg = "Duplicate user IDs detected in {$course} data: ";
312-
$msg .= implode(", ", $user_ids);
326+
$msg .= implode(", ", $duplicated_ids);
313327
$this->log_it($msg);
314328
}
315329
}

0 commit comments

Comments
 (0)