diff --git a/grade/import/csv/classes/load_data.php b/grade/import/csv/classes/load_data.php new file mode 100644 index 00000000000..3cbb069ae64 --- /dev/null +++ b/grade/import/csv/classes/load_data.php @@ -0,0 +1,608 @@ +. + +/** + * A class for loading and preparing grade data from import. + * + * @package gradeimport_csv + * @copyright 2014 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * A class for loading and preparing grade data from import. + * + * @package gradeimport_csv + * @copyright 2014 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class gradeimport_csv_load_data { + + /** @var string $error csv import error. */ + protected $error; + /** @var int $iid Unique identifier for these csv records. */ + protected $iid; + /** @var array $headers Column names for the data. */ + protected $headers; + /** @var array $previewdata A subsection of the csv imported data. */ + protected $previewdata; + + // The map_user_data_with_value variables. + /** @var array $newgrades Grades to be inserted into the gradebook. */ + protected $newgrades; + /** @var array $newfeedbacks Feedback to be inserted into the gradebook. */ + protected $newfeedbacks; + /** @var int $studentid Student ID*/ + protected $studentid; + + // The prepare_import_grade_data() variables. + /** @var bool $status The current status of the import. True = okay, False = errors. */ + protected $status; + /** @var int $importcode The code for this batch insert. */ + protected $importcode; + /** @var array $gradebookerrors An array of errors from trying to import into the gradebook. */ + protected $gradebookerrors; + /** @var array $newgradeitems An array of new grade items to be inserted into the gradebook. */ + protected $newgradeitems; + + /** + * Load CSV content for previewing. + * + * @param string $text The grade data being imported. + * @param string $encoding The type of encoding the file uses. + * @param string $separator The separator being used to define each field. + * @param int $previewrows How many rows are being previewed. + */ + public function load_csv_content($text, $encoding, $separator, $previewrows) { + $this->raise_limits(); + + $this->iid = csv_import_reader::get_new_iid('grade'); + $csvimport = new csv_import_reader($this->iid, 'grade'); + + $csvimport->load_csv_content($text, $encoding, $separator); + $this->error = $csvimport->get_error(); + + // Get header (field names). + $this->headers = $csvimport->get_columns(); + $this->trim_headers(); + + $csvimport->init(); + $this->previewdata = array(); + + for ($numlines = 0; $numlines <= $previewrows; $numlines++) { + $lines = $csvimport->next(); + if ($lines) { + $this->previewdata[] = $lines; + } + } + } + + /** + * Gets all of the grade items in this course. + * + * @param int $courseid Course id; + * @return array An array of grade items for the course. + */ + public static function fetch_grade_items($courseid) { + $gradeitems = null; + if ($allgradeitems = grade_item::fetch_all(array('courseid' => $courseid))) { + foreach ($allgradeitems as $gradeitem) { + // Skip course type and category type. + if ($gradeitem->itemtype == 'course' || $gradeitem->itemtype == 'category') { + continue; + } + + $displaystring = null; + if (!empty($gradeitem->itemmodule)) { + $displaystring = get_string('modulename', $gradeitem->itemmodule).get_string('labelsep', 'langconfig') + .$gradeitem->get_name(); + } else { + $displaystring = $gradeitem->get_name(); + } + $gradeitems[$gradeitem->id] = $displaystring; + } + } + return $gradeitems; + } + + /** + * Cleans the column headers from the CSV file. + */ + protected function trim_headers() { + foreach ($this->headers as $i => $h) { + $h = trim($h); // Remove whitespace. + $h = clean_param($h, PARAM_RAW); // Clean the header. + $this->headers[$i] = $h; + } + } + + /** + * Raises the php execution time and memory limits for importing the CSV file. + */ + protected function raise_limits() { + // Large files are likely to take their time and memory. Let PHP know + // that we'll take longer, and that the process should be recycled soon + // to free up memory. + core_php_time_limit::raise(); + raise_memory_limit(MEMORY_EXTRA); + } + + /** + * Inserts a record into the grade_import_values table. This also adds common record information. + * + * @param object $record The grade record being inserted into the database. + * @param int $studentid The student ID. + * @return bool|int true or insert id on success. Null if the grade value is too high. + */ + protected function insert_grade_record($record, $studentid) { + global $DB, $USER, $CFG; + $record->importcode = $this->importcode; + $record->userid = $studentid; + $record->importer = $USER->id; + // By default the maximum grade is 100. + $gradepointmaximum = 100; + // If the grade limit has been increased then use the gradepointmax setting. + if ($CFG->unlimitedgrades) { + $gradepointmaximum = $CFG->gradepointmax; + } + // If the record final grade is set then check that the grade value isn't too high. + // Final grade will not be set if we are inserting feedback. + if (!isset($record->finalgrade) || $record->finalgrade <= $gradepointmaximum) { + return $DB->insert_record('grade_import_values', $record); + } else { + $this->cleanup_import(get_string('gradevaluetoobig', 'grades', $gradepointmaximum)); + return null; + } + } + + /** + * Insert the new grade into the grade item buffer table. + * + * @param array $header The column headers from the CSV file. + * @param int $key Current row identifier. + * @param string $value The value for this row (final grade). + * @return array new grades that are ready for commiting to the gradebook. + */ + protected function import_new_grade_item($header, $key, $value) { + global $DB, $USER; + + // First check if header is already in temp database. + if (empty($this->newgradeitems[$key])) { + + $newgradeitem = new stdClass(); + $newgradeitem->itemname = $header[$key]; + $newgradeitem->importcode = $this->importcode; + $newgradeitem->importer = $USER->id; + + // Insert into new grade item buffer. + $this->newgradeitems[$key] = $DB->insert_record('grade_import_newitem', $newgradeitem); + } + $newgrade = new stdClass(); + $newgrade->newgradeitem = $this->newgradeitems[$key]; + + // If the user has a grade for this grade item. + if (trim($value) != '-') { + // Instead of omitting the grade we could insert one with finalgrade set to 0. + // We do not have access to grade item min grade. + $newgrade->finalgrade = $value; + $newgrades[] = $newgrade; + } + return $newgrades; + } + + /** + * Check that the user is in the system. + * + * @param string $value The value, from the csv file, being mapped to identify the user. + * @param array $userfields Contains the field and label being mapped from. + * @return int Returns the user ID if it exists, otherwise null. + */ + protected function check_user_exists($value, $userfields) { + global $DB; + + $usercheckproblem = false; + $user = null; + // The user may use the incorrect field to match the user. This could result in an exception. + try { + $user = $DB->get_record('user', array($userfields['field'] => $value)); + } catch (Exception $e) { + $usercheckproblem = true; + } + // Field may be fine, but no records were returned. + if (!$user || $usercheckproblem) { + $usermappingerrorobj = new stdClass(); + $usermappingerrorobj->field = $userfields['label']; + $usermappingerrorobj->value = $value; + $this->cleanup_import(get_string('usermappingerror', 'grades', $usermappingerrorobj)); + unset($usermappingerrorobj); + return null; + } + return $user->id; + } + + /** + * Check to see if the feedback matches a grade item. + * + * @param int $courseid The course ID. + * @param int $itemid The ID of the grade item that the feedback relates to. + * @param string $value The actual feedback being imported. + * @return object Creates a feedback object with the item ID and the feedback value. + */ + protected function create_feedback($courseid, $itemid, $value) { + // Case of an id, only maps id of a grade_item. + // This was idnumber. + if (!new grade_item(array('id' => $itemid, 'courseid' => $courseid))) { + // Supplied bad mapping, should not be possible since user + // had to pick mapping. + $this->cleanup_import(get_string('importfailed', 'grades')); + return null; + } + + // The itemid is the id of the grade item. + $feedback = new stdClass(); + $feedback->itemid = $itemid; + $feedback->feedback = $value; + return $feedback; + } + + /** + * This updates existing grade items. + * + * @param int $courseid The course ID. + * @param array $map Mapping information provided by the user. + * @param int $key The line that we are currently working on. + * @param bool $verbosescales Form setting for grading with scales. + * @param string $value The grade value . + * @return array grades to be updated. + */ + protected function update_grade_item($courseid, $map, $key, $verbosescales, $value) { + // Case of an id, only maps id of a grade_item. + // This was idnumber. + if (!$gradeitem = new grade_item(array('id' => $map[$key], 'courseid' => $courseid))) { + // Supplied bad mapping, should not be possible since user + // had to pick mapping. + $this->cleanup_import(get_string('importfailed', 'grades')); + return null; + } + + // Check if grade item is locked if so, abort. + if ($gradeitem->is_locked()) { + $this->cleanup_import(get_string('gradeitemlocked', 'grades')); + return null; + } + + $newgrade = new stdClass(); + $newgrade->itemid = $gradeitem->id; + if ($gradeitem->gradetype == GRADE_TYPE_SCALE and $verbosescales) { + if ($value === '' or $value == '-') { + $value = null; // No grade. + } else { + $scale = $gradeitem->load_scale(); + $scales = explode(',', $scale->scale); + $scales = array_map('trim', $scales); // Hack - trim whitespace around scale options. + array_unshift($scales, '-'); // Scales start at key 1. + $key = array_search($value, $scales); + if ($key === false) { + $this->cleanup_import(get_string('badgrade', 'grades')); + return null; + } + $value = $key; + } + $newgrade->finalgrade = $value; + } else { + if ($value === '' or $value == '-') { + $value = null; // No grade. + } else { + // If the value has a local decimal or can correctly be unformatted, do it. + $validvalue = unformat_float($value, true); + if ($validvalue !== false) { + $value = $validvalue; + } else { + // Non numeric grade value supplied, possibly mapped wrong column. + $this->cleanup_import(get_string('badgrade', 'grades')); + return null; + } + } + $newgrade->finalgrade = $value; + } + $this->newgrades[] = $newgrade; + return $this->newgrades; + } + + /** + * Clean up failed CSV grade import. Clears the temp table for inserting grades. + * + * @param string $notification The error message to display from the unsuccessful grade import. + */ + protected function cleanup_import($notification) { + $this->status = false; + import_cleanup($this->importcode); + $this->gradebookerrors[] = $notification; + } + + /** + * Check user mapping. + * + * @param string $mappingidentifier The user field that we are matching together. + * @param string $value The value we are checking / importing. + * @param array $header The column headers of the csv file. + * @param array $map Mapping information provided by the user. + * @param int $key Current row identifier. + * @param int $courseid The course ID. + * @param int $feedbackgradeid The ID of the grade item that the feedback relates to. + * @param bool $verbosescales Form setting for grading with scales. + */ + protected function map_user_data_with_value($mappingidentifier, $value, $header, $map, $key, $courseid, $feedbackgradeid, + $verbosescales) { + + // Fields that the user can be mapped from. + $userfields = array( + 'userid' => array( + 'field' => 'id', + 'label' => 'id', + ), + 'useridnumber' => array( + 'field' => 'idnumber', + 'label' => 'idnumber', + ), + 'useremail' => array( + 'field' => 'email', + 'label' => 'email address', + ), + 'username' => array( + 'field' => 'username', + 'label' => 'username', + ), + ); + + switch ($mappingidentifier) { + case 'userid': + case 'useridnumber': + case 'useremail': + case 'username': + // Skip invalid row with blank user field. + if (!empty($value)) { + $this->studentid = $this->check_user_exists($value, $userfields[$mappingidentifier]); + } + break; + case 'new': + $this->newgrades = $this->import_new_grade_item($header, $key, $value); + break; + case 'feedback': + if ($feedbackgradeid) { + $feedback = $this->create_feedback($courseid, $feedbackgradeid, $value); + if (isset($feedback)) { + $this->newfeedbacks[] = $feedback; + } + } + break; + default: + // Existing grade items. + if (!empty($map[$key])) { + $this->newgrades = $this->update_grade_item($courseid, $map, $key, $verbosescales, $value, + $mappingidentifier); + } + // Otherwise, we ignore this column altogether because user has chosen + // to ignore them (e.g. institution, address etc). + break; + } + } + + /** + * Checks and prepares grade data for inserting into the gradebook. + * + * @param array $header Column headers of the CSV file. + * @param object $formdata Mapping information from the preview page. + * @param object $csvimport csv import reader object for iterating over the imported CSV file. + * @param int $courseid The course ID. + * @param bool $separatemode If we have groups are they separate? + * @param mixed $currentgroup current group information. + * @param bool $verbosescales Form setting for grading with scales. + * @return bool True if the status for importing is okay, false if there are errors. + */ + public function prepare_import_grade_data($header, $formdata, $csvimport, $courseid, $separatemode, $currentgroup, + $verbosescales) { + global $DB, $USER; + + // The import code is used for inserting data into the grade tables. + $this->importcode = $formdata->importcode; + $this->status = true; + $this->headers = $header; + $this->studentid = null; + $this->gradebookerrors = null; + // Temporary array to keep track of what new headers are processed. + $this->newgradeitems = array(); + $this->trim_headers(); + + $map = array(); + // Loops mapping_0, mapping_1 .. mapping_n and construct $map array. + foreach ($header as $i => $head) { + if (isset($formdata->{'mapping_'.$i})) { + $map[$i] = $formdata->{'mapping_'.$i}; + } + } + + // If mapping information is supplied. + $map[clean_param($formdata->mapfrom, PARAM_RAW)] = clean_param($formdata->mapto, PARAM_RAW); + + // Check for mapto collisions. + $maperrors = array(); + foreach ($map as $i => $j) { + if ($j == 0) { + // You can have multiple ignores. + continue; + } else { + if (!isset($maperrors[$j])) { + $maperrors[$j] = true; + } else { + // Collision. + print_error('cannotmapfield', '', '', $j); + } + } + } + + $this->raise_limits(); + + $csvimport->init(); + + while ($line = $csvimport->next()) { + if (count($line) <= 1) { + // There is no data on this line, move on. + continue; + } + + // Array to hold all grades to be inserted. + $this->newgrades = array(); + // Array to hold all feedback. + $this->newfeedbacks = array(); + // Each line is a student record. + foreach ($line as $key => $value) { + + $value = clean_param($value, PARAM_RAW); + $value = trim($value); + + /* + * the options are + * 1) userid, useridnumber, usermail, username - used to identify user row + * 2) new - new grade item + * 3) id - id of the old grade item to map onto + * 3) feedback_id - feedback for grade item id + */ + + // Explode the mapping for feedback into a label 'feedback' and the identifying number. + $mappingbase = explode("_", $map[$key]); + $mappingidentifier = $mappingbase[0]; + // Set the feedback identifier if it exists. + if (isset($mappingbase[1])) { + $feedbackgradeid = (int)$mappingbase[1]; + } else { + $feedbackgradeid = ''; + } + + $this->map_user_data_with_value($mappingidentifier, $value, $header, $map, $key, $courseid, $feedbackgradeid, + $verbosescales); + if ($this->status === false) { + return $this->status; + } + } + + // No user mapping supplied at all, or user mapping failed. + if (empty($this->studentid) || !is_numeric($this->studentid)) { + // User not found, abort whole import. + $this->cleanup_import(get_string('usermappingerrorusernotfound', 'grades')); + break; + } + + if ($separatemode and !groups_is_member($currentgroup, $this->studentid)) { + // Not allowed to import into this group, abort. + $this->cleanup_import(get_string('usermappingerrorcurrentgroup', 'grades')); + break; + } + + // Insert results of this students into buffer. + if ($this->status and !empty($this->newgrades)) { + + foreach ($this->newgrades as $newgrade) { + + // Check if grade_grade is locked and if so, abort. + if (!empty($newgrade->itemid) and $gradegrade = new grade_grade(array('itemid' => $newgrade->itemid, + 'userid' => $this->studentid))) { + if ($gradegrade->is_locked()) { + // Individual grade locked. + $this->cleanup_import(get_string('gradelocked', 'grades')); + return $this->status; + } + } + $insertid = self::insert_grade_record($newgrade, $this->studentid); + // Check to see if the insert was successful. + if (empty($insertid)) { + return null; + } + } + } + + // Updating/inserting all comments here. + if ($this->status and !empty($this->newfeedbacks)) { + foreach ($this->newfeedbacks as $newfeedback) { + $sql = "SELECT * + FROM {grade_import_values} + WHERE importcode=? AND userid=? AND itemid=? AND importer=?"; + if ($feedback = $DB->get_record_sql($sql, array($this->importcode, $this->studentid, $newfeedback->itemid, + $USER->id))) { + $newfeedback->id = $feedback->id; + $DB->update_record('grade_import_values', $newfeedback); + + } else { + // The grade item for this is not updated. + $insertid = self::insert_grade_record($newfeedback, $this->studentid); + // Check to see if the insert was successful. + if (empty($insertid)) { + return null; + } + } + } + } + } + return $this->status; + } + + /** + * Returns the headers parameter for this class. + * + * @return array returns headers parameter for this class. + */ + public function get_headers() { + return $this->headers; + } + + /** + * Returns the error parameter for this class. + * + * @return string returns error parameter for this class. + */ + public function get_error() { + return $this->error; + } + + /** + * Returns the iid parameter for this class. + * + * @return int returns iid parameter for this class. + */ + public function get_iid() { + return $this->iid; + } + + /** + * Returns the preview_data parameter for this class. + * + * @return array returns previewdata parameter for this class. + */ + public function get_previewdata() { + return $this->previewdata; + } + + /** + * Returns the gradebookerrors parameter for this class. + * + * @return array returns gradebookerrors parameter for this class. + */ + public function get_gradebookerrors() { + return $this->gradebookerrors; + } +} diff --git a/grade/import/csv/classes/output/renderer.php b/grade/import/csv/classes/output/renderer.php new file mode 100644 index 00000000000..b8750fd13bc --- /dev/null +++ b/grade/import/csv/classes/output/renderer.php @@ -0,0 +1,90 @@ +. + +/** + * Renderers for the import of CSV files into the gradebook. + * + * @package gradeimport_csv + * @copyright 2014 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Renderers for the import of CSV files into the gradebook. + * + * @package gradeimport_csv + * @copyright 2014 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class gradeimport_csv_renderer extends plugin_renderer_base { + + /** + * A renderer for the standard upload file form. + * + * @param object $course The course we are doing all of this action in. + * @param object $mform The mform for uploading CSV files. + * @return string html to be displayed. + */ + public function standard_upload_file_form($course, $mform) { + + $output = groups_print_course_menu($course, 'index.php?id=' . $course->id, true); + $output .= html_writer::start_tag('div', array('class' => 'clearer')); + $output .= html_writer::end_tag('div'); + + // Form. + ob_start(); + $mform->display(); + $output .= ob_get_contents(); + ob_end_clean(); + + return $output; + } + + /** + * A renderer for the CSV file preview. + * + * @param array $header Column headers from the CSV file. + * @param array $data The rest of the data from the CSV file. + * @return string html to be displayed. + */ + public function import_preview_page($header, $data) { + + $html = $this->output->heading(get_string('importpreview', 'grades')); + + $table = new html_table(); + $table->head = $header; + $table->data = $data; + $html .= html_writer::table($table); + + return $html; + } + + /** + * A renderer for errors generated trying to import the CSV file. + * + * @param array $errors Display import errors. + * @return string errors as html to be displayed. + */ + public function errors($errors) { + $html = ''; + foreach ($errors as $error) { + $html .= $this->output->notification($error); + } + return $html; + } +} diff --git a/grade/import/csv/index.php b/grade/import/csv/index.php index 5a0dcdd2dda..21c72704ae0 100644 --- a/grade/import/csv/index.php +++ b/grade/import/csv/index.php @@ -22,7 +22,7 @@ require_once($CFG->dirroot. '/grade/import/grade_import_form.php'); require_once($CFG->dirroot.'/grade/import/lib.php'); require_once($CFG->libdir . '/csvlib.class.php'); -$id = required_param('id', PARAM_INT); // course id +$id = required_param('id', PARAM_INT); // Course id. $separator = optional_param('separator', '', PARAM_ALPHA); $verbosescales = optional_param('verbosescales', 1, PARAM_BOOL); $iid = optional_param('iid', null, PARAM_INT); @@ -46,92 +46,41 @@ $context = context_course::instance($id); require_capability('moodle/grade:import', $context); require_capability('gradeimport/csv:view', $context); -$separatemode = (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)); +$separatemode = (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and + !has_capability('moodle/site:accessallgroups', $context)); $currentgroup = groups_get_course_group($course); print_grade_page_head($course->id, 'import', 'csv', get_string('importcsv', 'grades')); -// Set up the grade import mapping form. -$gradeitems = array(); -if ($id) { - if ($grade_items = grade_item::fetch_all(array('courseid'=>$id))) { - foreach ($grade_items as $grade_item) { - // Skip course type and category type. - if ($grade_item->itemtype == 'course' || $grade_item->itemtype == 'category') { - continue; - } +$renderer = $PAGE->get_renderer('gradeimport_csv'); - $displaystring = null; - if (!empty($grade_item->itemmodule)) { - $displaystring = get_string('modulename', $grade_item->itemmodule).get_string('labelsep', 'langconfig') - .$grade_item->get_name(); - } else { - $displaystring = $grade_item->get_name(); - } - $gradeitems[$grade_item->id] = $displaystring; - } - } -} - -// Set up the import form. -$mform = new grade_import_form(null, array('includeseparator' => true, 'verbosescales' => true, 'acceptedtypes' => - array('.csv', '.txt'))); +// Get the grade items to be matched with the import mapping columns. +$gradeitems = gradeimport_csv_load_data::fetch_grade_items($course->id); // If the csv file hasn't been imported yet then look for a form submission or // show the initial submission form. if (!$iid) { + + // Set up the import form. + $mform = new grade_import_form(null, array('includeseparator' => true, 'verbosescales' => $verbosescales, 'acceptedtypes' => + array('.csv', '.txt'))); + // If the import form has been submitted. if ($formdata = $mform->get_data()) { - - // Large files are likely to take their time and memory. Let PHP know - // that we'll take longer, and that the process should be recycled soon - // to free up memory. - core_php_time_limit::raise(); - raise_memory_limit(MEMORY_EXTRA); - - // Use current (non-conflicting) time stamp. - $importcode = get_new_importcode(); - $text = $mform->get_file_content('userfile'); - $iid = csv_import_reader::get_new_iid('grade'); - $csvimport = new csv_import_reader($iid, 'grade'); - - $csvimport->load_csv_content($text, $formdata->encoding, $separator); - - // --- get header (field names) --- - $header = $csvimport->get_columns(); - - // Print a preview of the data. - $numlines = 0; // 0 lines previewed so far. - - echo $OUTPUT->heading(get_string('importpreview', 'grades')); - - foreach ($header as $i => $h) { - $h = trim($h); // Remove whitespace. - $h = clean_param($h, PARAM_RAW); // Clean the header. - $header[$i] = $h; + $csvimport = new gradeimport_csv_load_data(); + $csvimport->load_csv_content($text, $formdata->encoding, $separator, $formdata->previewrows); + $csvimporterror = $csvimport->get_error(); + if (!empty($csvimporterror)) { + echo $renderer->errors(array($csvimport->get_error())); + echo $OUTPUT->footer(); + die(); } - - $table = new html_table(); - $table->head = $header; - $csvimport->init(); - $previewdata = array(); - while ($numlines <= $formdata->previewrows) { - $lines = $csvimport->next(); - if ($lines) { - $previewdata[] = $lines; - } - $numlines ++; - } - $table->data = $previewdata; - echo html_writer::table($table); + $iid = $csvimport->get_iid(); + echo $renderer->import_preview_page($csvimport->get_headers(), $csvimport->get_previewdata()); } else { // Display the standard upload file form. - groups_print_course_menu($course, 'index.php?id='.$id); - echo html_writer::start_tag('div', array('class' => 'clearer')); - echo html_writer::end_tag('div'); - - $mform->display(); + echo $renderer->standard_upload_file_form($course, $mform); echo $OUTPUT->footer(); die(); } @@ -140,314 +89,37 @@ if (!$iid) { // Data has already been submitted so we can use the $iid to retrieve it. $csvimport = new csv_import_reader($iid, 'grade'); $header = $csvimport->get_columns(); +// Get a new import code for updating to the grade book. +if (empty($importcode)) { + $importcode = get_new_importcode(); +} +$mappingformdata = array( + 'gradeitems' => $gradeitems, + 'header' => $header, + 'iid' => $iid, + 'id' => $id, + 'importcode' => $importcode, + 'verbosescales' => $verbosescales +); // we create a form to handle mapping data from the file to the database. -$mform2 = new grade_import_mapping_form(null, array('gradeitems'=>$gradeitems, 'header'=>$header)); -$mform2->set_data(array('iid' => $iid, 'id' => $id, 'importcode'=>$importcode, 'verbosescales' => $verbosescales)); +$mform2 = new grade_import_mapping_form(null, $mappingformdata); // Here, if we have data, we process the fields and enter the information into the database. if ($formdata = $mform2->get_data()) { + $gradeimport = new gradeimport_csv_load_data(); + $status = $gradeimport->prepare_import_grade_data($header, $formdata, $csvimport, $course->id, $separatemode, + $currentgroup, $verbosescales); - foreach ($header as $i => $h) { - $h = trim($h); // Remove whitespace. - $h = clean_param($h, PARAM_RAW); // Clean the header. - $header[$i] = $h; - } - - $map = array(); - // loops mapping_0, mapping_1 .. mapping_n and construct $map array - foreach ($header as $i => $head) { - if (isset($formdata->{'mapping_'.$i})) { - $map[$i] = $formdata->{'mapping_'.$i}; - } - } - - // if mapping information is supplied - $map[clean_param($formdata->mapfrom, PARAM_RAW)] = clean_param($formdata->mapto, PARAM_RAW); - - // check for mapto collisions - $maperrors = array(); - foreach ($map as $i => $j) { - if ($j == 0) { - // you can have multiple ignores - continue; - } else { - if (!isset($maperrors[$j])) { - $maperrors[$j] = true; - } else { - // collision - print_error('cannotmapfield', '', '', $j); - } - } - } - - // Large files are likely to take their time and memory. Let PHP know - // that we'll take longer, and that the process should be recycled soon - // to free up memory. - core_php_time_limit::raise(); - raise_memory_limit(MEMORY_EXTRA); - - $userfields = array( - 'userid' => array( - 'field' => 'id', - 'label' => 'id', - ), - 'useridnumber' => array( - 'field' => 'idnumber', - 'label' => 'idnumber', - ), - 'useremail' => array( - 'field' => 'email', - 'label' => 'email address', - ), - 'username' => array( - 'field' => 'username', - 'label' => 'username', - ), - ); - - $csvimport->init(); - - $newgradeitems = array(); // temporary array to keep track of what new headers are processed - $status = true; - - while ($line = $csvimport->next()) { - if(count($line) <= 1){ - // there is no data on this line, move on - continue; - } - - // array to hold all grades to be inserted - $newgrades = array(); - // array to hold all feedback - $newfeedbacks = array(); - // each line is a student record - foreach ($line as $key => $value) { - - $value = clean_param($value, PARAM_RAW); - $value = trim($value); - - /* - * the options are - * 1) userid, useridnumber, usermail, username - used to identify user row - * 2) new - new grade item - * 3) id - id of the old grade item to map onto - * 3) feedback_id - feedback for grade item id - */ - - $t = explode("_", $map[$key]); - $t0 = $t[0]; - if (isset($t[1])) { - $t1 = (int)$t[1]; - } else { - $t1 = ''; - } - - switch ($t0) { - case 'userid': - case 'useridnumber': - case 'useremail': - case 'username': - // Skip invalid row with blank user field. - if (empty($value)) { - continue 3; - } - - if (!$user = $DB->get_record('user', array($userfields[$t0]['field'] => $value))) { - // User not found, abort whole import. - import_cleanup($importcode); - $usermappingerrorobj = new stdClass(); - $usermappingerrorobj->field = $userfields[$t0]['label']; - $usermappingerrorobj->value = $value; - echo $OUTPUT->notification(get_string('usermappingerror', 'grades', $usermappingerrorobj)); - unset($usermappingerrorobj); - $status = false; - break 3; - } - $studentid = $user->id; - break; - case 'new': - // first check if header is already in temp database - - if (empty($newgradeitems[$key])) { - - $newgradeitem = new stdClass(); - $newgradeitem->itemname = $header[$key]; - $newgradeitem->importcode = $importcode; - $newgradeitem->importer = $USER->id; - - // insert into new grade item buffer - $newgradeitems[$key] = $DB->insert_record('grade_import_newitem', $newgradeitem); - } - $newgrade = new stdClass(); - $newgrade->newgradeitem = $newgradeitems[$key]; - - // if the user has a grade for this grade item - if (trim($value) != '-') { - // instead of omitting the grade we could insert one with finalgrade set to 0 - // we do not have access to grade item min grade - $newgrade->finalgrade = $value; - $newgrades[] = $newgrade; - } - break; - case 'feedback': - if ($t1) { - // case of an id, only maps id of a grade_item - // this was idnumber - if (!$gradeitem = new grade_item(array('id'=>$t1, 'courseid'=>$course->id))) { - // supplied bad mapping, should not be possible since user - // had to pick mapping - $status = false; - import_cleanup($importcode); - // Relying on the default import failed message below. - break 3; - } - - // t1 is the id of the grade item - $feedback = new stdClass(); - $feedback->itemid = $t1; - $feedback->feedback = $value; - $newfeedbacks[] = $feedback; - } - break; - default: - // existing grade items - if (!empty($map[$key])) { - // case of an id, only maps id of a grade_item - // this was idnumber - if (!$gradeitem = new grade_item(array('id'=>$map[$key], 'courseid'=>$course->id))) { - // supplied bad mapping, should not be possible since user - // had to pick mapping - $status = false; - import_cleanup($importcode); - // Relying on the default import failed message below. - break 3; - } - - // check if grade item is locked if so, abort - if ($gradeitem->is_locked()) { - $status = false; - import_cleanup($importcode); - echo $OUTPUT->notification(get_string('gradeitemlocked', 'grades')); - break 3; - } - - $newgrade = new stdClass(); - $newgrade->itemid = $gradeitem->id; - if ($gradeitem->gradetype == GRADE_TYPE_SCALE and $verbosescales) { - if ($value === '' or $value == '-') { - $value = null; // no grade - } else { - $scale = $gradeitem->load_scale(); - $scales = explode(',', $scale->scale); - $scales = array_map('trim', $scales); //hack - trim whitespace around scale options - array_unshift($scales, '-'); // scales start at key 1 - $key = array_search($value, $scales); - if ($key === false) { - echo "
t0 is $t0"; - echo "
grade is $value"; - $status = false; - import_cleanup($importcode); - echo $OUTPUT->notification(get_string('badgrade', 'grades')); - break 3; - } - $value = $key; - } - $newgrade->finalgrade = $value; - } else { - if ($value === '' or $value == '-') { - $value = null; // No grade. - } else { - // If the value has a local decimal or can correctly be unformatted, do it. - $validvalue = unformat_float($value, true); - if ($validvalue !== false) { - $value = $validvalue; - } else { - // Non numeric grade value supplied, possibly mapped wrong column. - echo "
t0 is $t0"; - echo "
grade is $value"; - $status = false; - import_cleanup($importcode); - echo $OUTPUT->notification(get_string('badgrade', 'grades')); - break 3; - } - } - $newgrade->finalgrade = $value; - } - $newgrades[] = $newgrade; - } // otherwise, we ignore this column altogether - // because user has chosen to ignore them (e.g. institution, address etc) - break; - } - } - - // no user mapping supplied at all, or user mapping failed - if (empty($studentid) || !is_numeric($studentid)) { - // user not found, abort whole import - $status = false; - import_cleanup($importcode); - echo $OUTPUT->notification(get_string('usermappingerrorusernotfound', 'grades')); - break; - } - - if ($separatemode and !groups_is_member($currentgroup, $studentid)) { - // not allowed to import into this group, abort - $status = false; - import_cleanup($importcode); - echo $OUTPUT->notification(get_string('usermappingerrorcurrentgroup', 'grades')); - break; - } - - // insert results of this students into buffer - if ($status and !empty($newgrades)) { - - foreach ($newgrades as $newgrade) { - - // check if grade_grade is locked and if so, abort - if (!empty($newgrade->itemid) and $grade_grade = new grade_grade(array('itemid'=>$newgrade->itemid, 'userid'=>$studentid))) { - if ($grade_grade->is_locked()) { - // individual grade locked - $status = false; - import_cleanup($importcode); - echo $OUTPUT->notification(get_string('gradelocked', 'grades')); - break 2; - } - } - - $newgrade->importcode = $importcode; - $newgrade->userid = $studentid; - $newgrade->importer = $USER->id; - $DB->insert_record('grade_import_values', $newgrade); - } - } - - // updating/inserting all comments here - if ($status and !empty($newfeedbacks)) { - foreach ($newfeedbacks as $newfeedback) { - $sql = "SELECT * - FROM {grade_import_values} - WHERE importcode=? AND userid=? AND itemid=? AND importer=?"; - if ($feedback = $DB->get_record_sql($sql, array($importcode, $studentid, $newfeedback->itemid, $USER->id))) { - $newfeedback->id = $feedback->id; - $DB->update_record('grade_import_values', $newfeedback); - - } else { - // the grade item for this is not updated - $newfeedback->importcode = $importcode; - $newfeedback->userid = $studentid; - $newfeedback->importer = $USER->id; - $DB->insert_record('grade_import_values', $newfeedback); - } - } - } - } - - /// at this stage if things are all ok, we commit the changes from temp table + // At this stage if things are all ok, we commit the changes from temp table. if ($status) { grade_import_commit($course->id, $importcode); } else { - echo $OUTPUT->notification(get_string('importfailed', 'grades')); + $errors = $gradeimport->get_gradebookerrors(); + $errors[] = get_string('importfailed', 'grades'); + echo $renderer->errors($errors); } + echo $OUTPUT->footer(); } else { // If data hasn't been submitted then display the data mapping form. $mform2->display(); diff --git a/grade/import/csv/version.php b/grade/import/csv/version.php index a840138ce15..2db93ccf6e9 100644 --- a/grade/import/csv/version.php +++ b/grade/import/csv/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2014051200; // The current plugin version (Date: YYYYMMDDXX) +$plugin->version = 2014093000; // The current plugin version (Date: YYYYMMDDXX) $plugin->requires = 2014050800; // Requires this Moodle version $plugin->component = 'gradeimport_csv'; // Full name of the plugin (used for diagnostics) diff --git a/grade/import/grade_import_form.php b/grade/import/grade_import_form.php index 1aa5e7b5db9..1bc546eec6c 100644 --- a/grade/import/grade_import_form.php +++ b/grade/import/grade_import_form.php @@ -136,16 +136,22 @@ class grade_import_mapping_form extends moodleform { // course id needs to be passed for auth purposes $mform->addElement('hidden', 'map', 1); $mform->setType('map', PARAM_INT); - $mform->addElement('hidden', 'id'); + $mform->setConstant('map', 1); + $mform->addElement('hidden', 'id', $this->_customdata['id']); $mform->setType('id', PARAM_INT); - $mform->addElement('hidden', 'iid'); + $mform->setConstant('id', $this->_customdata['id']); + $mform->addElement('hidden', 'iid', $this->_customdata['iid']); $mform->setType('iid', PARAM_INT); - $mform->addElement('hidden', 'importcode'); + $mform->setConstant('iid', $this->_customdata['iid']); + $mform->addElement('hidden', 'importcode', $this->_customdata['importcode']); $mform->setType('importcode', PARAM_FILE); + $mform->setConstant('importcode', $this->_customdata['importcode']); $mform->addElement('hidden', 'verbosescales', 1); $mform->setType('verbosescales', PARAM_INT); + $mform->setConstant('verbosescales', $this->_customdata['verbosescales']); $mform->addElement('hidden', 'groupid', groups_get_course_group($COURSE)); $mform->setType('groupid', PARAM_INT); + $mform->setConstant('groupid', groups_get_course_group($COURSE)); $this->add_action_buttons(false, get_string('uploadgrades', 'grades')); } diff --git a/lang/en/grades.php b/lang/en/grades.php index 702e2787eb1..106b3af4431 100644 --- a/lang/en/grades.php +++ b/lang/en/grades.php @@ -312,6 +312,7 @@ $string['gradetype_help'] = 'There are 4 grade types: * Text - Feedback only Only value and scale grade types may be aggregated. The grade type for an activity-based grade item is set on the activity settings page.'; +$string['gradevaluetoobig'] = 'One of the grade values is larger than the allowed grade maximum of {$a}'; $string['gradeview'] = 'View grade'; $string['gradeweighthelp'] = 'Grade weight help'; $string['groupavg'] = 'Group average';