Skip to content

[Feature:System] Autofeed — Term Codes #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions student_auto_feed/ssaf_validate.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,6 @@ public static function validate_row($row, $row_num) : bool {
case $num_fields === $validate_num_fields:
self::$error = "Row {$row_num} has {$num_fields} columns. {$validate_num_fields} expected.";
return false;
// Check term code (skips when set to null).
case is_null(EXPECTED_TERM_CODE) ? true : $row[COLUMN_TERM_CODE] === EXPECTED_TERM_CODE:
self::$error = "Row {$row_num} failed validation for unexpected term code \"{$row[COLUMN_TERM_CODE]}\".";
return false;
// User ID must contain only alpha characters, numbers, underscore, hyphen, and period.
case boolval(preg_match("/^[a-z0-9_\-\.]+$/i", $row[COLUMN_USER_ID])):
self::$error = "Row {$row_num} failed user ID validation \"{$row[COLUMN_USER_ID]}\".";
Expand Down
119 changes: 83 additions & 36 deletions student_auto_feed/submitty_student_auto_feed.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,60 +158,97 @@ private function get_csv_data() {
$row_num = 1;
}

$graded_reg_codes = STUDENT_REGISTERED_CODES;
$audit_reg_codes = is_null(STUDENT_AUDIT_CODES) ? array() : STUDENT_AUDIT_CODES;
$latedrop_reg_codes = is_null(STUDENT_LATEDROP_CODES) ? array() : STUDENT_LATEDROP_CODES;
$all_valid_reg_codes = array_merge($graded_reg_codes, $audit_reg_codes, $latedrop_reg_codes);
$unexpected_term_codes = array();

// Read and assign csv rows into $this->data array
$row = fgetcsv($this->fh, 0, CSV_DELIM_CHAR);
while(!feof($this->fh)) {
//Trim whitespace from all fields in $row
// 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 = strtolower($row[COLUMN_COURSE_PREFIX] . $row[COLUMN_COURSE_NUMBER]);
switch(true) {
// Check that $row has an appropriate student registration.
case array_search($row[COLUMN_REGISTRATION], $all_valid_reg_codes) === false:
// Skip row.
break;

// Check that $row is associated with the current term (if check is enabled)
// Assume this check is OK, when EXPECTED_TERM_CODE is null (check disabled)
case is_null(EXPECTED_TERM_CODE) ? false : $row[COLUMN_TERM_CODE] !== EXPECTED_TERM_CODE:
// Note the unexpected term code for logging, if not already noted.
if (array_search($row[COLUMN_TERM_CODE], $unexpected_term_codes) === false) {
$unexpected_term_codes[] = $row[COLUMN_TERM_CODE];
}
break;

// Check that $row is associated with the course list.
case array_search($course, $this->course_list) !== false:
if (validate::validate_row($row, $row_num)) {
// Include $row
$this->data[$course][] = $row;

// Does $row have a valid registration code?
$graded_codes = STUDENT_REGISTERED_CODES;
$audit_codes = is_null(STUDENT_AUDIT_CODES) ? array() : STUDENT_AUDIT_CODES;
$latedrop_codes = is_null(STUDENT_LATEDROP_CODES) ? array() : STUDENT_LATEDROP_CODES;
$all_valid_codes = array_merge($graded_codes, $audit_codes, $latedrop_codes);
if (array_search($row[COLUMN_REGISTRATION], $all_valid_codes) !== false) {
// Check that $row is associated with the course list
if (array_search($course, $this->course_list) !== false) {
// $row with a blank email is included, but it is also logged.
if ($row[COLUMN_EMAIL] === "") {
$this->log_it("Blank email found for user {$row[COLUMN_USER_ID]}, row {$row_num}.");
}
} else {
// There is a problem with $row, so log the problem and skip.
$this->invalid_courses[$course] = true;
$this->log_it(validate::$error);
}
break;

// Check that the $row is associated with a mapped course.
case array_key_exists($course, $this->mapped_courses):
// Also verify that the section is mapped.
$section = $row[COLUMN_SECTION];
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)) {
$this->data[$course][] = $row;
// Rows with blank emails are allowed, but they are being logged.
// Include $row.
$row[COLUMN_SECTION] = $this->mapped_courses[$course][$section]['mapped_section'];
$this->data[$m_course][] = $row;

// $row with a blank email is allowed, but it is also logged.
if ($row[COLUMN_EMAIL] === "") {
$this->log_it("Blank email found for user {$row[COLUMN_USER_ID]}, row {$row_num}.");
}
} else {
$this->invalid_courses[$course] = true;
// There is a problem with $row, so log the problem and skip.
$this->invalid_courses[$m_course] = true;
$this->log_it(validate::$error);
}
// Instead, check that the $row is associated with mapped course
} else if (array_key_exists($course, $this->mapped_courses)) {
$section = $row[COLUMN_SECTION];
// Also verify that the section is mapped.
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)) {
$row[COLUMN_SECTION] = $this->mapped_courses[$course][$section]['mapped_section'];
$this->data[$m_course][] = $row;
// Rows with blank emails are allowed, but they are being logged.
if ($row[COLUMN_EMAIL] === "") {
$this->log_it("Blank email found for user {$row[COLUMN_USER_ID]}, row {$row_num}.");
}
} else {
$this->invalid_courses[$m_course] = true;
$this->log_it(validate::$error);
}
}
}
}
break;

default:
// Skip row by default.
break;

} // END switch (true)

$row = fgetcsv($this->fh, 0, CSV_DELIM_CHAR);
$row_num++;
}

// Log any unexpected term codes.
// This may provide a notice that the next term's data is available.
if (!empty($unexpected_term_codes)) {
$msg = "Unexpected term codes in CSV: ";
$msg .= implode(", ", $unexpected_term_codes);
$this->log_it($msg);
}

/* ---------------------------------------------------------------------
There may be "fake" or "practice" courses in Submitty that shouldn't be
altered by the autofeed. These courses will have no enrollments in the
Expand Down Expand Up @@ -262,6 +299,16 @@ private function check_for_duplicate_user_ids() {
return true;
}

/**
* An excessive ratio of dropped users may indicate bad data in the CSV.
*
* The confidence ratio is defined in config.php as VALIDATE_DROP_RATIO.
* Confidence value is a float between 0 and 1.0.
*
* @see validate::check_for_excessive_dropped_users() Found in ssaf_validate.php
*
* @return bool True when check is within confidence ratio. False otherwise.
*/
private function check_for_excessive_dropped_users() {
$is_validated = true;
$invalid_courses = array(); // intentional local array
Expand All @@ -280,10 +327,10 @@ private function check_for_excessive_dropped_users() {
array_unshift($invalid_courses, array('course' => "COURSE", 'diff' => "DIFF", 'ratio' => "RATIO")); // Header
foreach ($invalid_courses as $invalid_course) {
$msg .= " " .
str_pad($invalid_course['course'], 18, " ", STR_PAD_RIGHT) .
str_pad($invalid_course['diff'], 6, " ", STR_PAD_LEFT) .
str_pad($invalid_course['ratio'], 8, " ", STR_PAD_LEFT) .
PHP_EOL;
str_pad($invalid_course['course'], 18, " ", STR_PAD_RIGHT) .
str_pad($invalid_course['diff'], 6, " ", STR_PAD_LEFT) .
str_pad($invalid_course['ratio'], 8, " ", STR_PAD_LEFT) .
PHP_EOL;
}
$msg .= " No upsert performed on any/all courses in Submitty due to suspicious data sheet.";

Expand Down