See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * A class for loading and preparing grade data from import. 19 * 20 * @package gradeimport_csv 21 * @copyright 2014 Adrian Greeve <adrian@moodle.com> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 defined('MOODLE_INTERNAL') || die(); 26 27 /** 28 * A class for loading and preparing grade data from import. 29 * 30 * @package gradeimport_csv 31 * @copyright 2014 Adrian Greeve <adrian@moodle.com> 32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 */ 34 class gradeimport_csv_load_data { 35 36 /** @var string $error csv import error. */ 37 protected $error; 38 /** @var int $iid Unique identifier for these csv records. */ 39 protected $iid; 40 /** @var array $headers Column names for the data. */ 41 protected $headers; 42 /** @var array $previewdata A subsection of the csv imported data. */ 43 protected $previewdata; 44 45 // The map_user_data_with_value variables. 46 /** @var array $newgrades Grades to be inserted into the gradebook. */ 47 protected $newgrades; 48 /** @var array $newfeedbacks Feedback to be inserted into the gradebook. */ 49 protected $newfeedbacks; 50 /** @var int $studentid Student ID*/ 51 protected $studentid; 52 53 // The prepare_import_grade_data() variables. 54 /** @var bool $status The current status of the import. True = okay, False = errors. */ 55 protected $status; 56 /** @var int $importcode The code for this batch insert. */ 57 protected $importcode; 58 /** @var array $gradebookerrors An array of errors from trying to import into the gradebook. */ 59 protected $gradebookerrors; 60 /** @var array $newgradeitems An array of new grade items to be inserted into the gradebook. */ 61 protected $newgradeitems; 62 63 /** 64 * Load CSV content for previewing. 65 * 66 * @param string $text The grade data being imported. 67 * @param string $encoding The type of encoding the file uses. 68 * @param string $separator The separator being used to define each field. 69 * @param int $previewrows How many rows are being previewed. 70 */ 71 public function load_csv_content($text, $encoding, $separator, $previewrows) { 72 $this->raise_limits(); 73 74 $this->iid = csv_import_reader::get_new_iid('grade'); 75 $csvimport = new csv_import_reader($this->iid, 'grade'); 76 77 $csvimport->load_csv_content($text, $encoding, $separator); 78 $this->error = $csvimport->get_error(); 79 80 // If there are no import errors then proceed. 81 if (empty($this->error)) { 82 83 // Get header (field names). 84 $this->headers = $csvimport->get_columns(); 85 $this->trim_headers(); 86 87 $csvimport->init(); 88 $this->previewdata = array(); 89 90 for ($numlines = 0; $numlines <= $previewrows; $numlines++) { 91 $lines = $csvimport->next(); 92 if ($lines) { 93 $this->previewdata[] = $lines; 94 } 95 } 96 } 97 } 98 99 /** 100 * Gets all of the grade items in this course. 101 * 102 * @param int $courseid Course id; 103 * @return array An array of grade items for the course. 104 */ 105 public static function fetch_grade_items($courseid) { 106 $gradeitems = null; 107 if ($allgradeitems = grade_item::fetch_all(array('courseid' => $courseid))) { 108 foreach ($allgradeitems as $gradeitem) { 109 // Skip course type and category type. 110 if ($gradeitem->itemtype == 'course' || $gradeitem->itemtype == 'category') { 111 continue; 112 } 113 114 $displaystring = null; 115 if (!empty($gradeitem->itemmodule)) { 116 $displaystring = get_string('modulename', $gradeitem->itemmodule).get_string('labelsep', 'langconfig') 117 .$gradeitem->get_name(); 118 } else { 119 $displaystring = $gradeitem->get_name(); 120 } 121 $gradeitems[$gradeitem->id] = $displaystring; 122 } 123 } 124 return $gradeitems; 125 } 126 127 /** 128 * Cleans the column headers from the CSV file. 129 */ 130 protected function trim_headers() { 131 foreach ($this->headers as $i => $h) { 132 $h = trim($h); // Remove whitespace. 133 $h = clean_param($h, PARAM_RAW); // Clean the header. 134 $this->headers[$i] = $h; 135 } 136 } 137 138 /** 139 * Raises the php execution time and memory limits for importing the CSV file. 140 */ 141 protected function raise_limits() { 142 // Large files are likely to take their time and memory. Let PHP know 143 // that we'll take longer, and that the process should be recycled soon 144 // to free up memory. 145 core_php_time_limit::raise(); 146 raise_memory_limit(MEMORY_EXTRA); 147 } 148 149 /** 150 * Inserts a record into the grade_import_values table. This also adds common record information. 151 * 152 * @param stdClass $record The grade record being inserted into the database. 153 * @param int $studentid The student ID. 154 * @param grade_item $gradeitem Grade item. 155 * @return bool|int true or insert id on success. Null if the grade value is too high or too low or grade item not exist. 156 */ 157 protected function insert_grade_record(stdClass $record, int $studentid, grade_item $gradeitem) { 158 global $DB, $USER, $CFG; 159 $record->importcode = $this->importcode; 160 $record->userid = $studentid; 161 $record->importer = $USER->id; 162 // If the record final grade is set then check that the grade value isn't too high. 163 // Final grade will not be set if we are inserting feedback. 164 $gradepointmaximum = $gradeitem->grademax; 165 $gradepointminimum = $gradeitem->grademin; 166 167 $finalgradeinrange = 168 isset($record->finalgrade) && $record->finalgrade <= $gradepointmaximum && $record->finalgrade >= $gradepointminimum; 169 if (!isset($record->finalgrade) || $finalgradeinrange || $CFG->unlimitedgrades) { 170 return $DB->insert_record('grade_import_values', $record); 171 } else { 172 if ($record->finalgrade > $gradepointmaximum) { 173 $this->cleanup_import(get_string('gradevaluetoobig', 'grades', format_float($gradepointmaximum))); 174 } else { 175 $this->cleanup_import(get_string('gradevaluetoosmall', 'grades', format_float($gradepointminimum))); 176 } 177 return null; 178 } 179 } 180 181 /** 182 * Insert the new grade into the grade item buffer table. 183 * 184 * @param array $header The column headers from the CSV file. 185 * @param int $key Current row identifier. 186 * @param string $value The value for this row (final grade). 187 * @return stdClass new grade that is ready for commiting to the gradebook. 188 */ 189 protected function import_new_grade_item($header, $key, $value) { 190 global $DB, $USER; 191 192 // First check if header is already in temp database. 193 if (empty($this->newgradeitems[$key])) { 194 195 $newgradeitem = new stdClass(); 196 $newgradeitem->itemname = $header[$key]; 197 $newgradeitem->importcode = $this->importcode; 198 $newgradeitem->importer = $USER->id; 199 200 // Insert into new grade item buffer. 201 $this->newgradeitems[$key] = $DB->insert_record('grade_import_newitem', $newgradeitem); 202 } 203 $newgrade = new stdClass(); 204 $newgrade->newgradeitem = $this->newgradeitems[$key]; 205 206 $trimmed = trim($value); 207 if ($trimmed === '' or $trimmed == '-') { 208 // Blank or dash grade means null, ie "no grade". 209 $newgrade->finalgrade = null; 210 } else { 211 // We have an actual grade. 212 $newgrade->finalgrade = $value; 213 } 214 $this->newgrades[] = $newgrade; 215 return $newgrade; 216 } 217 218 /** 219 * Check that the user is in the system. 220 * 221 * @param string $value The value, from the csv file, being mapped to identify the user. 222 * @param array $userfields Contains the field and label being mapped from. 223 * @return int Returns the user ID if it exists, otherwise null. 224 */ 225 protected function check_user_exists($value, $userfields) { 226 global $DB; 227 228 $user = null; 229 $errorkey = false; 230 // The user may use the incorrect field to match the user. This could result in an exception. 231 try { 232 $field = $userfields['field']; 233 // Fields that can be queried in a case-insensitive manner. 234 $caseinsensitivefields = [ 235 'email', 236 'username', 237 ]; 238 // Build query predicate. 239 if (in_array($field, $caseinsensitivefields)) { 240 // Case-insensitive. 241 $select = $DB->sql_equal($field, ':' . $field, false); 242 } else { 243 // Exact-value. 244 $select = "{$field} = :{$field}"; 245 } 246 247 // Validate if the user id value is numerical. 248 if ($field === 'id' && !is_numeric($value)) { 249 $errorkey = 'usermappingerror'; 250 } 251 // Make sure the record exists and that there's only one matching record found. 252 $user = $DB->get_record_select('user', $select, array($userfields['field'] => $value), '*', MUST_EXIST); 253 } catch (dml_missing_record_exception $missingex) { 254 $errorkey = 'usermappingerror'; 255 } catch (dml_multiple_records_exception $multiex) { 256 $errorkey = 'usermappingerrormultipleusersfound'; 257 } 258 // Field may be fine, but no records were returned. 259 if ($errorkey) { 260 $usermappingerrorobj = new stdClass(); 261 $usermappingerrorobj->field = $userfields['label']; 262 $usermappingerrorobj->value = $value; 263 $this->cleanup_import(get_string($errorkey, 'grades', $usermappingerrorobj)); 264 unset($usermappingerrorobj); 265 return null; 266 } 267 return $user->id; 268 } 269 270 /** 271 * Check to see if the feedback matches a grade item. 272 * 273 * @param int $courseid The course ID. 274 * @param int $itemid The ID of the grade item that the feedback relates to. 275 * @param string $value The actual feedback being imported. 276 * @return object Creates a feedback object with the item ID and the feedback value. 277 */ 278 protected function create_feedback($courseid, $itemid, $value) { 279 // Case of an id, only maps id of a grade_item. 280 // This was idnumber. 281 if (!new grade_item(array('id' => $itemid, 'courseid' => $courseid))) { 282 // Supplied bad mapping, should not be possible since user 283 // had to pick mapping. 284 $this->cleanup_import(get_string('importfailed', 'grades')); 285 return null; 286 } 287 288 // The itemid is the id of the grade item. 289 $feedback = new stdClass(); 290 $feedback->itemid = $itemid; 291 $feedback->feedback = $value; 292 return $feedback; 293 } 294 295 /** 296 * This updates existing grade items. 297 * 298 * @param int $courseid The course ID. 299 * @param array $map Mapping information provided by the user. 300 * @param int $key The line that we are currently working on. 301 * @param bool $verbosescales Form setting for grading with scales. 302 * @param string $value The grade value. 303 * @return array grades to be updated. 304 */ 305 protected function update_grade_item($courseid, $map, $key, $verbosescales, $value) { 306 // Case of an id, only maps id of a grade_item. 307 // This was idnumber. 308 if (!$gradeitem = new grade_item(array('id' => $map[$key], 'courseid' => $courseid))) { 309 // Supplied bad mapping, should not be possible since user 310 // had to pick mapping. 311 $this->cleanup_import(get_string('importfailed', 'grades')); 312 return null; 313 } 314 315 // Check if grade item is locked if so, abort. 316 if ($gradeitem->is_locked()) { 317 $this->cleanup_import(get_string('gradeitemlocked', 'grades')); 318 return null; 319 } 320 321 $newgrade = new stdClass(); 322 $newgrade->itemid = $gradeitem->id; 323 if ($gradeitem->gradetype == GRADE_TYPE_SCALE and $verbosescales) { 324 if ($value === '' or $value == '-') { 325 $value = null; // No grade. 326 } else { 327 $scale = $gradeitem->load_scale(); 328 $scales = explode(',', $scale->scale); 329 $scales = array_map('trim', $scales); // Hack - trim whitespace around scale options. 330 array_unshift($scales, '-'); // Scales start at key 1. 331 $key = array_search($value, $scales); 332 if ($key === false) { 333 $this->cleanup_import(get_string('badgrade', 'grades')); 334 return null; 335 } 336 $value = $key; 337 } 338 $newgrade->finalgrade = $value; 339 } else { 340 if ($value === '' or $value == '-') { 341 $value = null; // No grade. 342 } else { 343 // If the value has a local decimal or can correctly be unformatted, do it. 344 $validvalue = unformat_float($value, true); 345 if ($validvalue !== false) { 346 $value = $validvalue; 347 } else { 348 // Non numeric grade value supplied, possibly mapped wrong column. 349 $this->cleanup_import(get_string('badgrade', 'grades')); 350 return null; 351 } 352 } 353 $newgrade->finalgrade = $value; 354 } 355 $this->newgrades[] = $newgrade; 356 return $this->newgrades; 357 } 358 359 /** 360 * Clean up failed CSV grade import. Clears the temp table for inserting grades. 361 * 362 * @param string $notification The error message to display from the unsuccessful grade import. 363 */ 364 protected function cleanup_import($notification) { 365 $this->status = false; 366 import_cleanup($this->importcode); 367 $this->gradebookerrors[] = $notification; 368 } 369 370 /** 371 * Check user mapping. 372 * 373 * @param string $mappingidentifier The user field that we are matching together. 374 * @param string $value The value we are checking / importing. 375 * @param array $header The column headers of the csv file. 376 * @param array $map Mapping information provided by the user. 377 * @param int $key Current row identifier. 378 * @param int $courseid The course ID. 379 * @param int $feedbackgradeid The ID of the grade item that the feedback relates to. 380 * @param bool $verbosescales Form setting for grading with scales. 381 */ 382 protected function map_user_data_with_value($mappingidentifier, $value, $header, $map, $key, $courseid, $feedbackgradeid, 383 $verbosescales) { 384 385 // Fields that the user can be mapped from. 386 $userfields = array( 387 'userid' => array( 388 'field' => 'id', 389 'label' => 'id', 390 ), 391 'useridnumber' => array( 392 'field' => 'idnumber', 393 'label' => 'idnumber', 394 ), 395 'useremail' => array( 396 'field' => 'email', 397 'label' => 'email address', 398 ), 399 'username' => array( 400 'field' => 'username', 401 'label' => 'username', 402 ), 403 ); 404 405 switch ($mappingidentifier) { 406 case 'userid': 407 case 'useridnumber': 408 case 'useremail': 409 case 'username': 410 $this->studentid = $this->check_user_exists($value, $userfields[$mappingidentifier]); 411 break; 412 case 'new': 413 $this->import_new_grade_item($header, $key, $value); 414 break; 415 case 'feedback': 416 if ($feedbackgradeid) { 417 $feedback = $this->create_feedback($courseid, $feedbackgradeid, $value); 418 if (isset($feedback)) { 419 $this->newfeedbacks[] = $feedback; 420 } 421 } 422 break; 423 default: 424 // Existing grade items. 425 if (!empty($map[$key])) { 426 $this->newgrades = $this->update_grade_item($courseid, $map, $key, $verbosescales, $value, 427 $mappingidentifier); 428 } 429 // Otherwise, we ignore this column altogether because user has chosen 430 // to ignore them (e.g. institution, address etc). 431 break; 432 } 433 } 434 435 /** 436 * Checks and prepares grade data for inserting into the gradebook. 437 * 438 * @param array $header Column headers of the CSV file. 439 * @param object $formdata Mapping information from the preview page. 440 * @param object $csvimport csv import reader object for iterating over the imported CSV file. 441 * @param int $courseid The course ID. 442 * @param bool $separatemode If we have groups are they separate? 443 * @param mixed $currentgroup current group information. 444 * @param bool $verbosescales Form setting for grading with scales. 445 * @return bool True if the status for importing is okay, false if there are errors. 446 */ 447 public function prepare_import_grade_data($header, $formdata, $csvimport, $courseid, $separatemode, $currentgroup, 448 $verbosescales) { 449 global $DB, $USER; 450 451 // The import code is used for inserting data into the grade tables. 452 $this->importcode = $formdata->importcode; 453 $this->status = true; 454 $this->headers = $header; 455 $this->studentid = null; 456 $this->gradebookerrors = null; 457 $forceimport = $formdata->forceimport; 458 // Temporary array to keep track of what new headers are processed. 459 $this->newgradeitems = array(); 460 $this->trim_headers(); 461 $timeexportkey = null; 462 $map = array(); 463 // Loops mapping_0, mapping_1 .. mapping_n and construct $map array. 464 foreach ($header as $i => $head) { 465 if (isset($formdata->{'mapping_'.$i})) { 466 $map[$i] = $formdata->{'mapping_'.$i}; 467 } 468 if ($head == get_string('timeexported', 'gradeexport_txt')) { 469 $timeexportkey = $i; 470 } 471 } 472 473 // If mapping information is supplied. 474 $map[clean_param($formdata->mapfrom, PARAM_RAW)] = clean_param($formdata->mapto, PARAM_RAW); 475 476 // Check for mapto collisions. 477 $maperrors = array(); 478 foreach ($map as $i => $j) { 479 if ($j == 0) { 480 // You can have multiple ignores. 481 continue; 482 } else { 483 if (!isset($maperrors[$j])) { 484 $maperrors[$j] = true; 485 } else { 486 // Collision. 487 throw new \moodle_exception('cannotmapfield', '', '', $j); 488 } 489 } 490 } 491 492 $this->raise_limits(); 493 494 $csvimport->init(); 495 496 while ($line = $csvimport->next()) { 497 if (count($line) <= 1) { 498 // There is no data on this line, move on. 499 continue; 500 } 501 502 // Array to hold all grades to be inserted. 503 $this->newgrades = array(); 504 // Array to hold all feedback. 505 $this->newfeedbacks = array(); 506 // Each line is a student record. 507 foreach ($line as $key => $value) { 508 509 $value = clean_param($value, PARAM_RAW); 510 $value = trim($value); 511 512 /* 513 * the options are 514 * 1) userid, useridnumber, usermail, username - used to identify user row 515 * 2) new - new grade item 516 * 3) id - id of the old grade item to map onto 517 * 3) feedback_id - feedback for grade item id 518 */ 519 520 // Explode the mapping for feedback into a label 'feedback' and the identifying number. 521 $mappingbase = explode("_", $map[$key]); 522 $mappingidentifier = $mappingbase[0]; 523 // Set the feedback identifier if it exists. 524 if (isset($mappingbase[1])) { 525 $feedbackgradeid = (int)$mappingbase[1]; 526 } else { 527 $feedbackgradeid = ''; 528 } 529 530 $this->map_user_data_with_value($mappingidentifier, $value, $header, $map, $key, $courseid, $feedbackgradeid, 531 $verbosescales); 532 if ($this->status === false) { 533 return $this->status; 534 } 535 } 536 537 // No user mapping supplied at all, or user mapping failed. 538 if (empty($this->studentid) || !is_numeric($this->studentid)) { 539 // User not found, abort whole import. 540 $this->cleanup_import(get_string('usermappingerrorusernotfound', 'grades')); 541 break; 542 } 543 544 if ($separatemode and !groups_is_member($currentgroup, $this->studentid)) { 545 // Not allowed to import into this group, abort. 546 $this->cleanup_import(get_string('usermappingerrorcurrentgroup', 'grades')); 547 break; 548 } 549 550 // Insert results of this students into buffer. 551 if ($this->status and !empty($this->newgrades)) { 552 553 foreach ($this->newgrades as $newgrade) { 554 555 // Check if grade_grade is locked and if so, abort. 556 if (!empty($newgrade->itemid) and $gradegrade = new grade_grade(array('itemid' => $newgrade->itemid, 557 'userid' => $this->studentid))) { 558 if ($gradegrade->is_locked()) { 559 // Individual grade locked. 560 $this->cleanup_import(get_string('gradelocked', 'grades')); 561 return $this->status; 562 } 563 // Check if the force import option is disabled and the last exported date column is present. 564 if (!$forceimport && !empty($timeexportkey)) { 565 $exportedtime = $line[$timeexportkey]; 566 if (clean_param($exportedtime, PARAM_INT) != $exportedtime || $exportedtime > time() || 567 $exportedtime < strtotime("-1 year", time())) { 568 // The date is invalid, or in the future, or more than a year old. 569 $this->cleanup_import(get_string('invalidgradeexporteddate', 'grades')); 570 return $this->status; 571 572 } 573 $timemodified = $gradegrade->get_dategraded(); 574 if (!empty($timemodified) && ($exportedtime < $timemodified)) { 575 // The item was graded after we exported it, we return here not to override it. 576 $user = core_user::get_user($this->studentid); 577 $this->cleanup_import(get_string('gradealreadyupdated', 'grades', fullname($user))); 578 return $this->status; 579 } 580 } 581 } 582 if (isset($newgrade->itemid)) { 583 $gradeitem = new grade_item(['id' => $newgrade->itemid]); 584 } else if (isset($newgrade->newgradeitem)) { 585 $gradeitem = new grade_item(['id' => $newgrade->newgradeitem]); 586 } 587 $insertid = isset($gradeitem) ? self::insert_grade_record($newgrade, $this->studentid, $gradeitem) : null; 588 // Check to see if the insert was successful. 589 if (empty($insertid)) { 590 return null; 591 } 592 } 593 } 594 595 // Updating/inserting all comments here. 596 if ($this->status and !empty($this->newfeedbacks)) { 597 foreach ($this->newfeedbacks as $newfeedback) { 598 $sql = "SELECT * 599 FROM {grade_import_values} 600 WHERE importcode=? AND userid=? AND itemid=? AND importer=?"; 601 if ($feedback = $DB->get_record_sql($sql, array($this->importcode, $this->studentid, $newfeedback->itemid, 602 $USER->id))) { 603 $newfeedback->id = $feedback->id; 604 $DB->update_record('grade_import_values', $newfeedback); 605 606 } else { 607 // The grade item for this is not updated. 608 $newfeedback->importonlyfeedback = true; 609 $insertid = self::insert_grade_record($newfeedback, $this->studentid, new grade_item(['id' => $newfeedback->itemid])); 610 // Check to see if the insert was successful. 611 if (empty($insertid)) { 612 return null; 613 } 614 } 615 } 616 } 617 } 618 return $this->status; 619 } 620 621 /** 622 * Returns the headers parameter for this class. 623 * 624 * @return array returns headers parameter for this class. 625 */ 626 public function get_headers() { 627 return $this->headers; 628 } 629 630 /** 631 * Returns the error parameter for this class. 632 * 633 * @return string returns error parameter for this class. 634 */ 635 public function get_error() { 636 return $this->error; 637 } 638 639 /** 640 * Returns the iid parameter for this class. 641 * 642 * @return int returns iid parameter for this class. 643 */ 644 public function get_iid() { 645 return $this->iid; 646 } 647 648 /** 649 * Returns the preview_data parameter for this class. 650 * 651 * @return array returns previewdata parameter for this class. 652 */ 653 public function get_previewdata() { 654 return $this->previewdata; 655 } 656 657 /** 658 * Returns the gradebookerrors parameter for this class. 659 * 660 * @return array returns gradebookerrors parameter for this class. 661 */ 662 public function get_gradebookerrors() { 663 return $this->gradebookerrors; 664 } 665 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body