From a9020543d2befdae36e0b952809d683b8546d54b Mon Sep 17 00:00:00 2001 From: pbailie Date: Tue, 9 Sep 2025 17:51:32 -0400 Subject: [PATCH 01/11] RCOS Mapping WIP Added placeholder for config.php. Add validation for credit load. --- student_auto_feed/config.php | 12 ++++++++++++ student_auto_feed/ssaf_validate.php | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/student_auto_feed/config.php b/student_auto_feed/config.php index 40432d7..fc97006 100644 --- a/student_auto_feed/config.php +++ b/student_auto_feed/config.php @@ -117,6 +117,7 @@ define('COLUMN_EMAIL', 4); //Student's Campus Email define('COLUMN_TERM_CODE', 11); //Semester code used in data validation define('COLUMN_REG_ID', 12); //Course and Section registration ID +define('COLUMN_CREDITS', 13); //Credits registered //Validate term code. Set to null to disable this check. define('EXPECTED_TERM_CODE', '201705'); @@ -127,6 +128,17 @@ //Set to true, if Submitty is using SAML for authentication. define('PROCESS_SAML', true); +/* RENSSELAER CENTER FOR OPEN SOURCE (RCOS) -------------------------------- */ + +//RCOS mapping is set true when all RCOS students are in the same CRN (course and section). +//When set true, RCOS students will be mapped to a section based on their credit load. e.g. 4 credits -> section 4. +//Set to false if either RCOS is not using Submitty or students are separated into different sections by credit load. +define('RCOS_MAPPING', false); + +//When RCOS mapping is true, set the course code for RCOS here. +//This is ignored when RCOS_MAPPING is false. +define('RCOS_COURSE_CODE', "csci4700"); + /* DATA SOURCING -------------------------------------------------------------- * The Student Autofeed provides helper scripts to retrieve the CSV file for * processing. Shell script ssaf.sh is used to invoke one of the helper diff --git a/student_auto_feed/ssaf_validate.php b/student_auto_feed/ssaf_validate.php index 80b0c0b..f87bf6a 100644 --- a/student_auto_feed/ssaf_validate.php +++ b/student_auto_feed/ssaf_validate.php @@ -68,6 +68,10 @@ public static function validate_row($row, $row_num) : bool { case boolval(preg_match("/^$|^(?![!#$%'*+\-\/=?^_`{|])[^(),:;<>@\\\"\[\]]+(? Date: Wed, 10 Sep 2025 19:35:51 -0400 Subject: [PATCH 02/11] Update submitty_student_auto_feed.php RCOS Mapping process --- student_auto_feed/submitty_student_auto_feed.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/student_auto_feed/submitty_student_auto_feed.php b/student_auto_feed/submitty_student_auto_feed.php index 9f1b9f2..582aace 100755 --- a/student_auto_feed/submitty_student_auto_feed.php +++ b/student_auto_feed/submitty_student_auto_feed.php @@ -212,6 +212,13 @@ private function get_csv_data() { // Check that $row is associated with the course list. case array_search($course, $this->course_list) !== false: if (validate::validate_row($row, $row_num)) { + // There is a special condition for RCOS where a student's credit load is mapped to their enrollment section. + // We need to check (1) we are mapping RCOS credits to section, and (2) AND this row is for an RCOS course. + // (RCOS only admits undergrads, so this will not happen in a mapped course) + if (RCOS_MAPPING && $course === RCOS_COURSE_CODE) { + $row[COLUMN_SECTION] = $row[COLUMN_CREDITS]; + } + // Include $row $this->data[$course][] = $row; From a43cf1fdea205bbb31a99ab95d781349cdb4c252 Mon Sep 17 00:00:00 2001 From: pbailie Date: Thu, 11 Sep 2025 19:16:29 -0400 Subject: [PATCH 03/11] Update submitty_student_auto_feed.php --- student_auto_feed/submitty_student_auto_feed.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/student_auto_feed/submitty_student_auto_feed.php b/student_auto_feed/submitty_student_auto_feed.php index 582aace..16f7a12 100755 --- a/student_auto_feed/submitty_student_auto_feed.php +++ b/student_auto_feed/submitty_student_auto_feed.php @@ -213,7 +213,7 @@ private function get_csv_data() { case array_search($course, $this->course_list) !== false: if (validate::validate_row($row, $row_num)) { // There is a special condition for RCOS where a student's credit load is mapped to their enrollment section. - // We need to check (1) we are mapping RCOS credits to section, and (2) AND this row is for an RCOS course. + // We need to check (1) we are mapping RCOS credits to section, and (2) AND this row is for the RCOS course. // (RCOS only admits undergrads, so this will not happen in a mapped course) if (RCOS_MAPPING && $course === RCOS_COURSE_CODE) { $row[COLUMN_SECTION] = $row[COLUMN_CREDITS]; From d3b56103f92374d25db3452522c5483939b92997 Mon Sep 17 00:00:00 2001 From: pbailie Date: Mon, 15 Sep 2025 14:48:00 -0400 Subject: [PATCH 04/11] Fixes --- student_auto_feed/readme.md | 6 +++--- student_auto_feed/ssaf_validate.php | 2 +- student_auto_feed/submitty_student_auto_feed.php | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/student_auto_feed/readme.md b/student_auto_feed/readme.md index 720714d..076207f 100644 --- a/student_auto_feed/readme.md +++ b/student_auto_feed/readme.md @@ -10,9 +10,9 @@ policies and practices.__ Detailed instructions can be found at [http://submitty.org/sysadmin/student\_auto\_feed](http://submitty.org/sysadmin/student_auto_feed) -Requirements: PHP 7.3 or higher with pgsql extension. `imap_remote.php` also -requires the imap extension. This system is intended to be platform agnostic, -but has been developed and tested with Ubuntu Linux. +Requires the pgsql extension. `imap_remote.php` also requires the imap extension. +This system is intended to be platform agnostic, but has been developed and tested +with Ubuntu Linux. ## submitty\_student\_auto\_feed.php A command line executable script to read a student enrollment data CSV file and diff --git a/student_auto_feed/ssaf_validate.php b/student_auto_feed/ssaf_validate.php index f87bf6a..6d551f4 100644 --- a/student_auto_feed/ssaf_validate.php +++ b/student_auto_feed/ssaf_validate.php @@ -69,7 +69,7 @@ public static function validate_row($row, $row_num) : bool { self::$error = "Row {$row_num} failed validation for student email \"{$row[COLUMN_EMAIL]}\"."; return false; // When RCOS_MAPPING is true, credit load must be between 1 and 4. Skip this check when RCOS_MAPPING is false. - case !(RCOS_MAPPING && !boolval(preg_match("/^[1-4]$/", $row['COLUMN_CREDITS']))): + case !(RCOS_MAPPING && !boolval(preg_match("/^[1-4]$/", $row[COLUMN_CREDITS]))): self::$error = "Row {$row_num} failed validation for RCOS credit load at \"{$row[COLUMN_CREDITS]}\"."; return false; } diff --git a/student_auto_feed/submitty_student_auto_feed.php b/student_auto_feed/submitty_student_auto_feed.php index 16f7a12..e3b9ac7 100755 --- a/student_auto_feed/submitty_student_auto_feed.php +++ b/student_auto_feed/submitty_student_auto_feed.php @@ -5,7 +5,7 @@ * * This script will read a student enrollment CSV feed provided by the campus * registrar or data warehouse and "upsert" (insert/update) the feed into - * Submitty's course databases. Requires PHP 7.3 and pgsql extension. + * Submitty's course databases. Requires pgsql extension. * * @author Peter Bailie, Rensselaer Polytechnic Institute */ @@ -217,6 +217,7 @@ private function get_csv_data() { // (RCOS only admits undergrads, so this will not happen in a mapped course) if (RCOS_MAPPING && $course === RCOS_COURSE_CODE) { $row[COLUMN_SECTION] = $row[COLUMN_CREDITS]; + print $row[COLUMN_SECTION]; } // Include $row From 1675d80f2b1fc9c786e7daa89166eee075a9521d Mon Sep 17 00:00:00 2001 From: pbailie Date: Wed, 17 Sep 2025 14:49:32 -0400 Subject: [PATCH 05/11] Update submitty_student_auto_feed.php --- student_auto_feed/submitty_student_auto_feed.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/student_auto_feed/submitty_student_auto_feed.php b/student_auto_feed/submitty_student_auto_feed.php index e3b9ac7..d3512c4 100755 --- a/student_auto_feed/submitty_student_auto_feed.php +++ b/student_auto_feed/submitty_student_auto_feed.php @@ -185,15 +185,18 @@ private function get_csv_data() { // Read and assign csv rows into $this->data array $row = fgetcsv($this->fh, 0, CSV_DELIM_CHAR); while(!feof($this->fh)) { - // Course is comprised of an alphabetic prefix and a numeric suffix. - $course = strtolower($row[COLUMN_COURSE_PREFIX] . $row[COLUMN_COURSE_NUMBER]); - // Trim whitespace from all fields in $row. array_walk($row, function(&$val, $key) { $val = trim($val); }); // Remove any leading zeroes from "integer" registration sections. if (ctype_digit($row[COLUMN_SECTION])) $row[COLUMN_SECTION] = ltrim($row[COLUMN_SECTION], "0"); + // Course is comprised of an alphabetic prefix and a numeric suffix. + $course = strtolower($row[COLUMN_COURSE_PREFIX] . $row[COLUMN_COURSE_NUMBER]); + + // Ensure RCOS's course code is in the same case as $course. + $rcos = strtolower(RCOS_COURSE_CODE); + switch(true) { // Check that $row has an appropriate student registration. case array_search($row[COLUMN_REGISTRATION], $all_valid_reg_codes) === false: @@ -215,9 +218,8 @@ private function get_csv_data() { // There is a special condition for RCOS where a student's credit load is mapped to their enrollment section. // We need to check (1) we are mapping RCOS credits to section, and (2) AND this row is for the RCOS course. // (RCOS only admits undergrads, so this will not happen in a mapped course) - if (RCOS_MAPPING && $course === RCOS_COURSE_CODE) { + if (RCOS_MAPPING && $course === $rcos) { $row[COLUMN_SECTION] = $row[COLUMN_CREDITS]; - print $row[COLUMN_SECTION]; } // Include $row From 048fbce3208db748ae91499a8eeb97146d0431f8 Mon Sep 17 00:00:00 2001 From: pbailie Date: Fri, 19 Sep 2025 16:18:10 -0400 Subject: [PATCH 06/11] Reverting Solution was incorrect as request was not properly communicated. --- student_auto_feed/config.php | 11 ----------- student_auto_feed/ssaf_validate.php | 4 ---- student_auto_feed/submitty_student_auto_feed.php | 10 ---------- 3 files changed, 25 deletions(-) diff --git a/student_auto_feed/config.php b/student_auto_feed/config.php index fc97006..4bcdbda 100644 --- a/student_auto_feed/config.php +++ b/student_auto_feed/config.php @@ -128,17 +128,6 @@ //Set to true, if Submitty is using SAML for authentication. define('PROCESS_SAML', true); -/* RENSSELAER CENTER FOR OPEN SOURCE (RCOS) -------------------------------- */ - -//RCOS mapping is set true when all RCOS students are in the same CRN (course and section). -//When set true, RCOS students will be mapped to a section based on their credit load. e.g. 4 credits -> section 4. -//Set to false if either RCOS is not using Submitty or students are separated into different sections by credit load. -define('RCOS_MAPPING', false); - -//When RCOS mapping is true, set the course code for RCOS here. -//This is ignored when RCOS_MAPPING is false. -define('RCOS_COURSE_CODE', "csci4700"); - /* DATA SOURCING -------------------------------------------------------------- * The Student Autofeed provides helper scripts to retrieve the CSV file for * processing. Shell script ssaf.sh is used to invoke one of the helper diff --git a/student_auto_feed/ssaf_validate.php b/student_auto_feed/ssaf_validate.php index 6d551f4..80b0c0b 100644 --- a/student_auto_feed/ssaf_validate.php +++ b/student_auto_feed/ssaf_validate.php @@ -68,10 +68,6 @@ public static function validate_row($row, $row_num) : bool { case boolval(preg_match("/^$|^(?![!#$%'*+\-\/=?^_`{|])[^(),:;<>@\\\"\[\]]+(?course_list) !== false: if (validate::validate_row($row, $row_num)) { - // There is a special condition for RCOS where a student's credit load is mapped to their enrollment section. - // We need to check (1) we are mapping RCOS credits to section, and (2) AND this row is for the RCOS course. - // (RCOS only admits undergrads, so this will not happen in a mapped course) - if (RCOS_MAPPING && $course === $rcos) { - $row[COLUMN_SECTION] = $row[COLUMN_CREDITS]; - } - // Include $row $this->data[$course][] = $row; From 9556ae0126ff9d74640c97921ee3f13aa7e89f5a Mon Sep 17 00:00:00 2001 From: pbailie Date: Tue, 23 Sep 2025 18:20:30 -0400 Subject: [PATCH 07/11] WIP --- student_auto_feed/config.php | 11 +++++ student_auto_feed/ssaf_rcos.php | 28 ++++++++++++ .../submitty_student_auto_feed.php | 44 ++++++++++++------- 3 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 student_auto_feed/ssaf_rcos.php diff --git a/student_auto_feed/config.php b/student_auto_feed/config.php index 4bcdbda..51d66fd 100644 --- a/student_auto_feed/config.php +++ b/student_auto_feed/config.php @@ -128,6 +128,17 @@ //Set to true, if Submitty is using SAML for authentication. define('PROCESS_SAML', true); +/* RENSSELAER CENTER FOR OPEN SOURCE (RCOS) -------------------------------- + * RCOS is not just one course, but several. Some of these courses also + * permit a student to declare their credit load. The data feed will need + * a column showing a student's credit load. See above: COLUMN_CREDITS + * + * DO NOT MAP RCOS COURSES IN THE SUBMITTY DATABASE + */ + +// List all RCOS courses, as an array. If you are not tracking RCOS, then set as null or an empty array. +define('RCOS_COURSE_LIST', null); + /* DATA SOURCING -------------------------------------------------------------- * The Student Autofeed provides helper scripts to retrieve the CSV file for * processing. Shell script ssaf.sh is used to invoke one of the helper diff --git a/student_auto_feed/ssaf_rcos.php b/student_auto_feed/ssaf_rcos.php new file mode 100644 index 0000000..c9c0292 --- /dev/null +++ b/student_auto_feed/ssaf_rcos.php @@ -0,0 +1,28 @@ +course_list = RCOS_COURSE_LIST ?? []; + array_walk($this->course_list, function(&$v, $i) { $v = strtolower($v); }); + } + + /** Adjusts `$row[COLUMN_SECTION]` when `$course` is an RCOS course. */ + public function map(string $course, array &$row): void { + if ($this->check($course)) { + $row[COLUMN_SECTION] = "{$row[COLUMN_COURSE_NUMBER]}-{$row[COLUMN_CREDITS]}"; + } + } + + /** Returns `true` if `$course` is an RCOS course and `false` otherwise. */ + public function check(string $course): bool { + return array_search($course, self::$course_list) !== false; + } +} diff --git a/student_auto_feed/submitty_student_auto_feed.php b/student_auto_feed/submitty_student_auto_feed.php index 8b825e0..70f583e 100755 --- a/student_auto_feed/submitty_student_auto_feed.php +++ b/student_auto_feed/submitty_student_auto_feed.php @@ -15,6 +15,7 @@ require __DIR__ . "/ssaf_cli.php"; require __DIR__ . "/ssaf_db.php"; require __DIR__ . "/ssaf_validate.php"; +require __DIR__ . "/ssaf_rcos.php"; // Important: Make sure we are running from CLI if (php_sapi_name() !== "cli") { @@ -27,22 +28,24 @@ /** primary process class */ class submitty_student_auto_feed { - /** @var resource File handle to read CSV */ - private $fh; - /** @var string Semester code */ - private $semester; - /** @var array List of courses registered in Submitty */ - private $course_list; - /** @var array Describes how courses are mapped from one to another */ - private $mapped_courses; - /** @var array Describes courses/sections that are duplicated to other courses/sections */ - private $crn_copymap; - /** @var array Courses with invalid data. */ - private $invalid_courses; - /** @var array All CSV data to be upserted */ - private $data; - /** @var string Ongoing string of messages to write to logfile */ - private $log_msg_queue; + /** File handle to read CSV */ + private resource $fh; + /** Semester code */ + private string $semester; + /** List of courses registered in Submitty */ + private array $course_list; + /** Describes how courses are mapped from one to another */ + private array $mapped_courses; + /** Describes courses/sections that are duplicated to other courses/sections */ + private array $crn_copymap; + /** Courses with invalid data. */ + private array $invalid_courses; + /** All CSV data to be upserted */ + private array $data; + /** Ongoing string of messages to write to logfile */ + private string $log_msg_queue; + /** For special cases involving Renssealer Center for Open Source */ + private object $rcos; /** Init properties. Open DB connection. Open CSV file. */ public function __construct() { @@ -80,6 +83,9 @@ public function __construct() { exit(1); } + // Helper object for special-cases involving RCOS. + $this->rcos = new rcos(); + // Get course list $error = null; $this->course_list = db::get_course_list($this->semester, $error); @@ -97,6 +103,9 @@ public function __construct() { exit(1); } + // RCOS courses should not be mapped in the database. + + // Get CRN shared courses/sections (when a course/section is copied to another course/section) $this->crn_copymap = $this->read_crn_copymap(); @@ -194,6 +203,9 @@ private function get_csv_data() { // Course is comprised of an alphabetic prefix and a numeric suffix. $course = strtolower($row[COLUMN_COURSE_PREFIX] . $row[COLUMN_COURSE_NUMBER]); + // Check/perform special-case RCOS registration section mapping. + rcos::map($course, $row); + switch(true) { // Check that $row has an appropriate student registration. case array_search($row[COLUMN_REGISTRATION], $all_valid_reg_codes) === false: From 8459787bccb06abb62f019365cc3aa10dc7bcd56 Mon Sep 17 00:00:00 2001 From: pbailie Date: Wed, 24 Sep 2025 19:56:20 -0400 Subject: [PATCH 08/11] RCOS course mapping Code refactoring, fixes, and comment/documentation improvements. --- student_auto_feed/config.php | 9 +++++-- student_auto_feed/ssaf_rcos.php | 17 +++++++------ .../submitty_student_auto_feed.php | 24 ++++++++++--------- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/student_auto_feed/config.php b/student_auto_feed/config.php index 51d66fd..c8555df 100644 --- a/student_auto_feed/config.php +++ b/student_auto_feed/config.php @@ -133,10 +133,15 @@ * permit a student to declare their credit load. The data feed will need * a column showing a student's credit load. See above: COLUMN_CREDITS * - * DO NOT MAP RCOS COURSES IN THE SUBMITTY DATABASE + * RCOS courses need to be mapped to a "primary" course in the database, + * but the autofeed will override the registration section as + * "{course_code}-{credit_load}". e.g. "csci4700-4". */ -// List all RCOS courses, as an array. If you are not tracking RCOS, then set as null or an empty array. +// 1. List all RCOS courses, as an array. If you are not tracking RCOS, then set as null or an empty array. +// 2. One (any which one) of these courses needs to be designated as "primary" and the others need to be mapped to the +// primary within the database. Registration sections need to be defined in the database, but are otherwise irrelevant +// as the auto feed will override registration sections. define('RCOS_COURSE_LIST', null); /* DATA SOURCING -------------------------------------------------------------- diff --git a/student_auto_feed/ssaf_rcos.php b/student_auto_feed/ssaf_rcos.php index c9c0292..7ff9e14 100644 --- a/student_auto_feed/ssaf_rcos.php +++ b/student_auto_feed/ssaf_rcos.php @@ -4,6 +4,13 @@ /** * Static utilty class to support RCOS (Rensselaer Center for Open Source) * + * This will override enrollment registration sections for RCOS courses. Some RCOS students may declare how many + * credits they are registered for, but are otherwise all placed in the same course and registration section. + * (currently) Submitty does not keep records for regsitered credits per student, but this record is needed for + * RCOS. Therefore, this helper class will override a RCOS student's enrollment record to `{course}-{credits}` + * e.g. `csci4700-4`. This must done while processing the CSV because every override requires the student's + * registered credits from the CSV. + * * @author Peter Bailie */ class rcos { @@ -11,18 +18,14 @@ class rcos { public function __construct() { $this->course_list = RCOS_COURSE_LIST ?? []; + sort($this->course_list, SORT_STRING); array_walk($this->course_list, function(&$v, $i) { $v = strtolower($v); }); } /** Adjusts `$row[COLUMN_SECTION]` when `$course` is an RCOS course. */ public function map(string $course, array &$row): void { - if ($this->check($course)) { - $row[COLUMN_SECTION] = "{$row[COLUMN_COURSE_NUMBER]}-{$row[COLUMN_CREDITS]}"; + if (in_array($course, $this->course_list, true)) { + $row[COLUMN_SECTION] = "{$course}-{$row[COLUMN_CREDITS]}"; } } - - /** Returns `true` if `$course` is an RCOS course and `false` otherwise. */ - public function check(string $course): bool { - return array_search($course, self::$course_list) !== false; - } } diff --git a/student_auto_feed/submitty_student_auto_feed.php b/student_auto_feed/submitty_student_auto_feed.php index 70f583e..1c55455 100755 --- a/student_auto_feed/submitty_student_auto_feed.php +++ b/student_auto_feed/submitty_student_auto_feed.php @@ -29,7 +29,7 @@ /** primary process class */ class submitty_student_auto_feed { /** File handle to read CSV */ - private resource $fh; + private $fh; /** Semester code */ private string $semester; /** List of courses registered in Submitty */ @@ -83,9 +83,6 @@ public function __construct() { exit(1); } - // Helper object for special-cases involving RCOS. - $this->rcos = new rcos(); - // Get course list $error = null; $this->course_list = db::get_course_list($this->semester, $error); @@ -103,12 +100,12 @@ public function __construct() { exit(1); } - // RCOS courses should not be mapped in the database. - - // Get CRN shared courses/sections (when a course/section is copied to another course/section) $this->crn_copymap = $this->read_crn_copymap(); + // Helper object for special-cases involving RCOS. + $this->rcos = new rcos(); + // Init other properties. $this->invalid_courses = []; $this->data = []; @@ -203,9 +200,6 @@ private function get_csv_data() { // Course is comprised of an alphabetic prefix and a numeric suffix. $course = strtolower($row[COLUMN_COURSE_PREFIX] . $row[COLUMN_COURSE_NUMBER]); - // Check/perform special-case RCOS registration section mapping. - rcos::map($course, $row); - switch(true) { // Check that $row has an appropriate student registration. case array_search($row[COLUMN_REGISTRATION], $all_valid_reg_codes) === false: @@ -224,6 +218,9 @@ private function get_csv_data() { // Check that $row is associated with the course list. case array_search($course, $this->course_list) !== false: if (validate::validate_row($row, $row_num)) { + // Check (and perform) special-case RCOS registration section mapping. + $this->rcos->map($course, $row); + // Include $row $this->data[$course][] = $row; @@ -245,8 +242,13 @@ private function get_csv_data() { if (array_key_exists($section, $this->mapped_courses[$course])) { $m_course = $this->mapped_courses[$course][$section]['mapped_course']; if (validate::validate_row($row, $row_num)) { - // Include $row. + // Do course mapping (alters registration section). $row[COLUMN_SECTION] = $this->mapped_courses[$course][$section]['mapped_section']; + + // Check (and override) for special-case RCOS registration section mapping. + $this->rcos->map($course, $row); + + // Include $row. $this->data[$m_course][] = $row; // $row with a blank email is allowed, but it is also logged. From e4c56ac9809d4e4bcad78a043b35de4abc1fd32f Mon Sep 17 00:00:00 2001 From: pbailie Date: Thu, 25 Sep 2025 19:35:55 -0400 Subject: [PATCH 09/11] RCOS section mapping Work in progress --- student_auto_feed/config.php | 19 +++++++++++-------- student_auto_feed/ssaf_rcos.php | 13 ++++++------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/student_auto_feed/config.php b/student_auto_feed/config.php index c8555df..a66aa3d 100644 --- a/student_auto_feed/config.php +++ b/student_auto_feed/config.php @@ -128,20 +128,23 @@ //Set to true, if Submitty is using SAML for authentication. define('PROCESS_SAML', true); -/* RENSSELAER CENTER FOR OPEN SOURCE (RCOS) -------------------------------- +/* RENSSELAER CENTER FOR OPEN SOURCE (RCOS) ----------------------------------- * RCOS is not just one course, but several. Some of these courses also * permit a student to declare their credit load. The data feed will need * a column showing a student's credit load. See above: COLUMN_CREDITS * - * RCOS courses need to be mapped to a "primary" course in the database, - * but the autofeed will override the registration section as - * "{course_code}-{credit_load}". e.g. "csci4700-4". + * Create only one RCOS course in Submitty, which will show up in the + * grader's/instructor's course list. The other RCOS courses must be mapped to + * this first course. Registration sections do need to be fully mapped, as the + * database does not permit mapping NULL sections. However, the upsert process + * will override how RCOS enrollments are translated, so that registration + * sections are, per student, "{course}-{credits}" e.g. J. Doe is enrolled in + * RCOS course CSCI4700 for 4 credits. They will be listed as enrolled in + * registration section "CSCI4700-4" */ -// 1. List all RCOS courses, as an array. If you are not tracking RCOS, then set as null or an empty array. -// 2. One (any which one) of these courses needs to be designated as "primary" and the others need to be mapped to the -// primary within the database. Registration sections need to be defined in the database, but are otherwise irrelevant -// as the auto feed will override registration sections. +// List *ALL* RCOS courses, as an array. +// If you are not tracking RCOS, then set this as null or an empty array. define('RCOS_COURSE_LIST', null); /* DATA SOURCING -------------------------------------------------------------- diff --git a/student_auto_feed/ssaf_rcos.php b/student_auto_feed/ssaf_rcos.php index 7ff9e14..fbe122b 100644 --- a/student_auto_feed/ssaf_rcos.php +++ b/student_auto_feed/ssaf_rcos.php @@ -4,12 +4,10 @@ /** * Static utilty class to support RCOS (Rensselaer Center for Open Source) * - * This will override enrollment registration sections for RCOS courses. Some RCOS students may declare how many - * credits they are registered for, but are otherwise all placed in the same course and registration section. - * (currently) Submitty does not keep records for regsitered credits per student, but this record is needed for - * RCOS. Therefore, this helper class will override a RCOS student's enrollment record to `{course}-{credits}` - * e.g. `csci4700-4`. This must done while processing the CSV because every override requires the student's - * registered credits from the CSV. + * This will override enrollment registration sections for RCOS courses to `{course}-{credits}` e.g. `CSCI4700-4`. + * Some RCOS students may declare how many credits they are registering for, so normal course mapping in the database + * is insufficient. This must done while processing the CSV because every override requires each student's registered + * credits from the CSV. * * @author Peter Bailie */ @@ -18,13 +16,14 @@ class rcos { public function __construct() { $this->course_list = RCOS_COURSE_LIST ?? []; - sort($this->course_list, SORT_STRING); array_walk($this->course_list, function(&$v, $i) { $v = strtolower($v); }); + sort($this->course_list, SORT_STRING); } /** Adjusts `$row[COLUMN_SECTION]` when `$course` is an RCOS course. */ public function map(string $course, array &$row): void { if (in_array($course, $this->course_list, true)) { + $course = strtoupper($course); $row[COLUMN_SECTION] = "{$course}-{$row[COLUMN_CREDITS]}"; } } From 2fda99148a98db01fc9af6db8340fad60465b459 Mon Sep 17 00:00:00 2001 From: pbailie Date: Thu, 25 Sep 2025 19:47:00 -0400 Subject: [PATCH 10/11] Update config.php --- student_auto_feed/config.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/student_auto_feed/config.php b/student_auto_feed/config.php index a66aa3d..5fddcb9 100644 --- a/student_auto_feed/config.php +++ b/student_auto_feed/config.php @@ -3,9 +3,7 @@ /* HEADING --------------------------------------------------------------------- * * config.php script used by submitty_student_auto_feed - * By Peter Bailie, Systems Programmer (RPI dept of computer science) - * - * Requires minimum PHP version 7.3 with pgsql extension. + * By Peter Bailie, Renssealer Polytechnic Institute * * Configuration of submitty_student_auto_feed is structured through a series * of named constants. From dd4e4bc507f837623dd090d19c8aa0799f577200 Mon Sep 17 00:00:00 2001 From: pbailie Date: Fri, 3 Oct 2025 21:38:56 -0400 Subject: [PATCH 11/11] Fix rare edge case bug There was a rare edge case that an email address found in the enrollment data, that didn't match RPI's 5+1 user ID pattern, could potentially cause the wrong entry(ies) to be filtered out when filtering duplicate student enrollments. Filtering code has been improved and the bug fixed. --- student_auto_feed/ssaf_validate.php | 42 ------------------ .../submitty_student_auto_feed.php | 44 +++++++++---------- 2 files changed, 22 insertions(+), 64 deletions(-) diff --git a/student_auto_feed/ssaf_validate.php b/student_auto_feed/ssaf_validate.php index 80b0c0b..f2dcaa1 100644 --- a/student_auto_feed/ssaf_validate.php +++ b/student_auto_feed/ssaf_validate.php @@ -74,48 +74,6 @@ public static function validate_row($row, $row_num) : bool { return true; } - /** - * Check $rows for duplicate user IDs. - * - * Submitty's master DB does not permit students to register more than once - * for any course. It would trigger a key violation exception. This - * function checks for data anomalies where a student shows up in a course - * more than once as that is indicative of an issue with CSV file data. - * Returns TRUE, as in no error, when $rows has all unique user IDs. - * False, as in error found, otherwise. $user_ids is filled when return - * is FALSE. - * - * @param array $rows Data rows to check (presumably an entire couse). - * @param string[] &$user_id Duplicated user ID, when found. - * @param string[] &$d_rows Rows containing duplicate user IDs, indexed by user ID. - * @return bool TRUE when all user IDs are unique, FALSE otherwise. - */ - public static function check_for_duplicate_user_ids(array $rows, &$user_ids, &$d_rows) : bool { - usort($rows, function($a, $b) { return $a[COLUMN_USER_ID] <=> $b[COLUMN_USER_ID]; }); - - $user_ids = []; - $d_rows = []; - $are_all_unique = true; // Unless proven FALSE - $length = count($rows); - for ($i = 1; $i < $length; $i++) { - $j = $i - 1; - if ($rows[$i][COLUMN_USER_ID] === $rows[$j][COLUMN_USER_ID]) { - $are_all_unique = false; - $user_id = $rows[$i][COLUMN_USER_ID]; - $user_ids[] = $user_id; - $d_rows[$user_id][] = $j; - $d_rows[$user_id][] = $i; - } - } - - foreach($d_rows as &$d_row) { - array_unique($d_row, SORT_REGULAR); - } - unset($d_row); - - return $are_all_unique; - } - /** * Validate that there isn't an excessive drop ratio in course enrollments. * diff --git a/student_auto_feed/submitty_student_auto_feed.php b/student_auto_feed/submitty_student_auto_feed.php index 1c55455..cf726ba 100755 --- a/student_auto_feed/submitty_student_auto_feed.php +++ b/student_auto_feed/submitty_student_auto_feed.php @@ -141,8 +141,8 @@ public function go() { case $this->check_for_excessive_dropped_users(): // This check will block all upserts when an error is detected. exit(1); - case $this->check_for_duplicate_user_ids(): - $this->log_it("Duplicate user IDs detected in CSV file."); + case $this->filter_duplicate_registrations(): + // Never returns false. Error messages are already in log queue. break; case $this->invalidate_courses(): // Should do nothing when $this->invalid_courses is empty @@ -299,31 +299,31 @@ private function get_csv_data() { } /** - * Users cannot be registered to the same course multiple times. + * Students cannot be registered to the same course multiple times. * - * Any course with a user registered more than once is flagged invalid as - * it is indicative of data errors from the CSV file. - * - * @return bool always TRUE + * If multiple registrations for the same student and course are found, the first instance is allowed to be + * upserted to the database. All other instances are removed from the data set and therefore not upserted. */ - private function check_for_duplicate_user_ids() { - foreach($this->data as $course => $rows) { - $user_ids = null; - $d_rows = null; - // Returns FALSE (as in there is an error) when duplicate IDs are found. - // However, a duplicate ID does not invalidate a course. Instead, the - // first enrollment is accepted, the other enrollments are discarded, - // and the event is logged. - if (validate::check_for_duplicate_user_ids($rows, $user_ids, $d_rows) === false) { - foreach($d_rows as $user_id => $userid_rows) { - $length = count($userid_rows); - for ($i = 1; $i < $length; $i++) { - unset($this->data[$course][$userid_rows[$i]]); - } + private function filter_duplicate_registrations(): true { + foreach($this->data as $course => &$rows) { + usort($rows, function($a, $b) { return $a[COLUMN_USER_ID] <=> $b[COLUMN_USER_ID]; }); + $duplicated_ids = []; + $num_rows = count($rows); + + // We are iterating from bottom to top through a course's data set. Should we find a duplicate registration + // and unset it from the array, (1) we are unsetting duplicates starting from the bottom, (2) which preserves + // the first entry among duplicate entries, and (3) we do not make a comparison with a null key. + for ($j = $num_rows - 1, $i = $j - 1; $i >= 0; $i--, $j--) { + if ($rows[$i][COLUMN_USER_ID] === $rows[$j][COLUMN_USER_ID]) { + $duplicated_ids[] = $rows[$j][COLUMN_USER_ID]; + unset($rows[$j]); } + } + if (count($duplicated_ids) > 0) { + array_unique($duplicated_ids, SORT_STRING); $msg = "Duplicate user IDs detected in {$course} data: "; - $msg .= implode(", ", $user_ids); + $msg .= implode(", ", $duplicated_ids); $this->log_it($msg); } }