Differences Between: [Versions 311 and 401] [Versions 311 and 402] [Versions 311 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 object $record The grade record being inserted into the database. 153 * @param int $studentid The student ID. 154 * @return bool|int true or insert id on success. Null if the grade value is too high. 155 */ 156 protected function insert_grade_record($record, $studentid) { 157 global $DB, $USER, $CFG; 158 $record->importcode = $this->importcode; 159 $record->userid = $studentid; 160 $record->importer = $USER->id; 161 // By default the maximum grade is 100. 162 $gradepointmaximum = 100; 163 // If the grade limit has been increased then use the gradepointmax setting. 164 if ($CFG->unlimitedgrades) { 165 $gradepointmaximum = $CFG->gradepointmax; 166 } 167 // If the record final grade is set then check that the grade value isn't too high. 168 // Final grade will not be set if we are inserting feedback. 169 if (!isset($record->finalgrade) || $record->finalgrade <= $gradepointmaximum) { 170 return $DB->insert_record('grade_import_values', $record); 171 } else { 172 $this->cleanup_import(get_string('gradevaluetoobig', 'grades', $gradepointmaximum)); 173 return null; 174 } 175 } 176 177 /** 178 * Insert the new grade into the grade item buffer table. 179 * 180 * @param array $header The column headers from the CSV file. 181 * @param int $key Current row identifier. 182 * @param string $value The value for this row (final grade). 183 * @return stdClass new grade that is ready for commiting to the gradebook. 184 */ 185 protected function import_new_grade_item($header, $key, $value) { 186 global $DB, $USER; 187 188 // First check if header is already in temp database. 189 if (empty($this->newgradeitems[$key])) { 190 191 $newgradeitem = new stdClass(); 192 $newgradeitem->itemname = $header[$key]; 193 $newgradeitem->importcode = $this->importcode; 194 $newgradeitem->importer = $USER->id; 195 196 // Insert into new grade item buffer. 197 $this->newgradeitems[$key] = $DB->insert_record('grade_import_newitem', $newgradeitem); 198 } 199 $newgrade = new stdClass(); 200 $newgrade->newgradeitem = $this->newgradeitems[$key]; 201 202 $trimmed = trim($value); 203 if ($trimmed === '' or $trimmed == '-') { 204 // Blank or dash grade means null, ie "no grade". 205 $newgrade->finalgrade = null; 206 } else { 207 // We have an actual grade. 208 $newgrade->finalgrade = $value; 209 } 210 $this->newgrades[] = $newgrade; 211 return $newgrade; 212 } 213 214 /** 215 * Check that the user is in the system. 216 * 217 * @param string $value The value, from the csv file, being mapped to identify the user. 218 * @param array $userfields Contains the field and label being mapped from. 219 * @return int Returns the user ID if it exists, otherwise null. 220 */ 221 protected function check_user_exists($value, $userfields) { 222 global $DB; 223 224 $user = null; 225 $errorkey = false; 226 // The user may use the incorrect field to match the user. This could result in an exception. 227 try { 228 $field = $userfields['field']; 229 // Fields that can be queried in a case-insensitive manner. 230 $caseinsensitivefields = [ 231 'email', 232 'username', 233 ]; 234 // Build query predicate. 235 if (in_array($field, $caseinsensitivefields)) { 236 // Case-insensitive. 237 $select = $DB->sql_equal($field, ':' . $field, false); 238 } else { 239 // Exact-value. 240 $select = "{$field} = :{$field}"; 241 } 242 243 // Validate if the user id value is numerical. 244 if ($field === 'id' && !is_numeric($value)) { 245 $errorkey = 'usermappingerror'; 246 } 247 // Make sure the record exists and that there's only one matching record found. 248 $user = $DB->get_record_select('user', $select, array($userfields['field'] => $value), '*', MUST_EXIST); 249 } catch (dml_missing_record_exception $missingex) { 250 $errorkey = 'usermappingerror'; 251 } catch (dml_multiple_records_exception $multiex) { 252 $errorkey = 'usermappingerrormultipleusersfound'; 253 } 254 // Field may be fine, but no records were returned. 255 if ($errorkey) { 256 $usermappingerrorobj = new stdClass(); 257 $usermappingerrorobj->field = $userfields['label']; 258 $usermappingerrorobj->value = $value; 259 $this->cleanup_import(get_string($errorkey, 'grades', $usermappingerrorobj)); 260 unset($usermappingerrorobj); 261 return null; 262 } 263 return $user->id; 264 } 265 266 /** 267 * Check to see if the feedback matches a grade item. 268 * 269 * @param int $courseid The course ID. 270 * @param int $itemid The ID of the grade item that the feedback relates to. 271 * @param string $value The actual feedback being imported. 272 * @return object Creates a feedback object with the item ID and the feedback value. 273 */ 274 protected function create_feedback($courseid, $itemid, $value) { 275 // Case of an id, only maps id of a grade_item. 276 // This was idnumber. 277 if (!new grade_item(array('id' => $itemid, 'courseid' => $courseid))) { 278 // Supplied bad mapping, should not be possible since user 279 // had to pick mapping. 280 $this->cleanup_import(get_string('importfailed', 'grades')); 281 return null; 282 } 283 284 // The itemid is the id of the grade item. 285 $feedback = new stdClass(); 286 $feedback->itemid = $itemid; 287 $feedback->feedback = $value; 288 return $feedback; 289 } 290 291 /** 292 * This updates existing grade items. 293 * 294 * @param int $courseid The course ID. 295 * @param array $map Mapping information provided by the user. 296 * @param int $key The line that we are currently working on. 297 * @param bool $verbosescales Form setting for grading with scales. 298 * @param string $value The grade value. 299 * @return array grades to be updated. 300 */ 301 protected function update_grade_item($courseid, $map, $key, $verbosescales, $value) { 302 // Case of an id, only maps id of a grade_item. 303 // This was idnumber. 304 if (!$gradeitem = new grade_item(array('id' => $map[$key], 'courseid' => $courseid))) { 305 // Supplied bad mapping, should not be possible since user 306 // had to pick mapping. 307 $this->cleanup_import(get_string('importfailed', 'grades')); 308 return null; 309 } 310 311 // Check if grade item is locked if so, abort. 312 if ($gradeitem->is_locked()) { 313 $this->cleanup_import(get_string('gradeitemlocked', 'grades')); 314 return null; 315 } 316 317 $newgrade = new stdClass(); 318 $newgrade->itemid = $gradeitem->id; 319 if ($gradeitem->gradetype == GRADE_TYPE_SCALE and $verbosescales) { 320 if ($value === '' or $value == '-') { 321 $value = null; // No grade. 322 } else { 323 $scale = $gradeitem->load_scale(); 324 $scales = explode(',', $scale->scale); 325 $scales = array_map('trim', $scales); // Hack - trim whitespace around scale options. 326 array_unshift($scales, '-'); // Scales start at key 1. 327 $key = array_search($value, $scales); 328 if ($key === false) { 329 $this->cleanup_import(get_string('badgrade', 'grades')); 330 return null; 331 } 332 $value = $key; 333 } 334 $newgrade->finalgrade = $value; 335 } else { 336 if ($value === '' or $value == '-') { 337 $value = null; // No grade. 338 } else { 339 // If the value has a local decimal or can correctly be unformatted, do it. 340 $validvalue = unformat_float($value, true); 341 if ($validvalue !== false) { 342 $value = $validvalue; 343 } else { 344 // Non numeric grade value supplied, possibly mapped wrong column. 345 $this->cleanup_import(get_string('badgrade', 'grades')); 346 return null; 347 } 348 } 349 $newgrade->finalgrade = $value; 350 } 351 $this->newgrades[] = $newgrade; 352 return $this->newgrades; 353 } 354 355 /** 356 * Clean up failed CSV grade import. Clears the temp table for inserting grades. 357 * 358 * @param string $notification The error message to display from the unsuccessful grade import. 359 */ 360 protected function cleanup_import($notification) { 361 $this->status = false; 362 import_cleanup($this->importcode); 363 $this->gradebookerrors[] = $notification; 364 } 365 366 /** 367 * Check user mapping. 368 * 369 * @param string $mappingidentifier The user field that we are matching together. 370 * @param string $value The value we are checking / importing. 371 * @param array $header The column headers of the csv file. 372 * @param array $map Mapping information provided by the user. 373 * @param int $key Current row identifier. 374 * @param int $courseid The course ID. 375 * @param int $feedbackgradeid The ID of the grade item that the feedback relates to. 376 * @param bool $verbosescales Form setting for grading with scales. 377 */ 378 protected function map_user_data_with_value($mappingidentifier, $value, $header, $map, $key, $courseid, $feedbackgradeid, 379 $verbosescales) { 380 381 // Fields that the user can be mapped from. 382 $userfields = array( 383 'userid' => array( 384 'field' => 'id', 385 'label' => 'id', 386 ), 387 'useridnumber' => array( 388 'field' => 'idnumber', 389 'label' => 'idnumber', 390 ), 391 'useremail' => array( 392 'field' => 'email', 393 'label' => 'email address', 394 ), 395 'username' => array( 396 'field' => 'username', 397 'label' => 'username', 398 ), 399 ); 400 401 switch ($mappingidentifier) { 402 case 'userid': 403 case 'useridnumber': 404 case 'useremail': 405 case 'username': 406 $this->studentid = $this->check_user_exists($value, $userfields[$mappingidentifier]); 407 break; 408 case 'new': 409 $this->import_new_grade_item($header, $key, $value); 410 break; 411 case 'feedback': 412 if ($feedbackgradeid) { 413 $feedback = $this->create_feedback($courseid, $feedbackgradeid, $value); 414 if (isset($feedback)) { 415 $this->newfeedbacks[] = $feedback; 416 } 417 } 418 break; 419 default: 420 // Existing grade items. 421 if (!empty($map[$key])) { 422 $this->newgrades = $this->update_grade_item($courseid, $map, $key, $verbosescales, $value, 423 $mappingidentifier); 424 } 425 // Otherwise, we ignore this column altogether because user has chosen 426 // to ignore them (e.g. institution, address etc). 427 break; 428 } 429 } 430 431 /** 432 * Checks and prepares grade data for inserting into the gradebook. 433 * 434 * @param array $header Column headers of the CSV file. 435 * @param object $formdata Mapping information from the preview page. 436 * @param object $csvimport csv import reader object for iterating over the imported CSV file. 437 * @param int $courseid The course ID. 438 * @param bool $separatemode If we have groups are they separate? 439 * @param mixed $currentgroup current group information. 440 * @param bool $verbosescales Form setting for grading with scales. 441 * @return bool True if the status for importing is okay, false if there are errors. 442 */ 443 public function prepare_import_grade_data($header, $formdata, $csvimport, $courseid, $separatemode, $currentgroup, 444 $verbosescales) { 445 global $DB, $USER; 446 447 // The import code is used for inserting data into the grade tables. 448 $this->importcode = $formdata->importcode; 449 $this->status = true; 450 $this->headers = $header; 451 $this->studentid = null; 452 $this->gradebookerrors = null; 453 $forceimport = $formdata->forceimport; 454 // Temporary array to keep track of what new headers are processed. 455 $this->newgradeitems = array(); 456 $this->trim_headers(); 457 $timeexportkey = null; 458 $map = array(); 459 // Loops mapping_0, mapping_1 .. mapping_n and construct $map array. 460 foreach ($header as $i => $head) { 461 if (isset($formdata->{'mapping_'.$i})) { 462 $map[$i] = $formdata->{'mapping_'.$i}; 463 } 464 if ($head == get_string('timeexported', 'gradeexport_txt')) { 465 $timeexportkey = $i; 466 } 467 } 468 469 // If mapping information is supplied. 470 $map[clean_param($formdata->mapfrom, PARAM_RAW)] = clean_param($formdata->mapto, PARAM_RAW); 471 472 // Check for mapto collisions. 473 $maperrors = array(); 474 foreach ($map as $i => $j) { 475 if ($j == 0) { 476 // You can have multiple ignores. 477 continue; 478 } else { 479 if (!isset($maperrors[$j])) { 480 $maperrors[$j] = true; 481 } else { 482 // Collision. 483 print_error('cannotmapfield', '', '', $j); 484 } 485 } 486 } 487 488 $this->raise_limits(); 489 490 $csvimport->init(); 491 492 while ($line = $csvimport->next()) { 493 if (count($line) <= 1) { 494 // There is no data on this line, move on. 495 continue; 496 } 497 498 // Array to hold all grades to be inserted. 499 $this->newgrades = array(); 500 // Array to hold all feedback. 501 $this->newfeedbacks = array(); 502 // Each line is a student record. 503 foreach ($line as $key => $value) { 504 505 $value = clean_param($value, PARAM_RAW); 506 $value = trim($value); 507 508 /* 509 * the options are 510 * 1) userid, useridnumber, usermail, username - used to identify user row 511 * 2) new - new grade item 512 * 3) id - id of the old grade item to map onto 513 * 3) feedback_id - feedback for grade item id 514 */ 515 516 // Explode the mapping for feedback into a label 'feedback' and the identifying number. 517 $mappingbase = explode("_", $map[$key]); 518 $mappingidentifier = $mappingbase[0]; 519 // Set the feedback identifier if it exists. 520 if (isset($mappingbase[1])) { 521 $feedbackgradeid = (int)$mappingbase[1]; 522 } else { 523 $feedbackgradeid = ''; 524 } 525 526 $this->map_user_data_with_value($mappingidentifier, $value, $header, $map, $key, $courseid, $feedbackgradeid, 527 $verbosescales); 528 if ($this->status === false) { 529 return $this->status; 530 } 531 } 532 533 // No user mapping supplied at all, or user mapping failed. 534 if (empty($this->studentid) || !is_numeric($this->studentid)) { 535 // User not found, abort whole import. 536 $this->cleanup_import(get_string('usermappingerrorusernotfound', 'grades')); 537 break; 538 } 539 540 if ($separatemode and !groups_is_member($currentgroup, $this->studentid)) { 541 // Not allowed to import into this group, abort. 542 $this->cleanup_import(get_string('usermappingerrorcurrentgroup', 'grades')); 543 break; 544 } 545 546 // Insert results of this students into buffer. 547 if ($this->status and !empty($this->newgrades)) { 548 549 foreach ($this->newgrades as $newgrade) { 550 551 // Check if grade_grade is locked and if so, abort. 552 if (!empty($newgrade->itemid) and $gradegrade = new grade_grade(array('itemid' => $newgrade->itemid, 553 'userid' => $this->studentid))) { 554 if ($gradegrade->is_locked()) { 555 // Individual grade locked. 556 $this->cleanup_import(get_string('gradelocked', 'grades')); 557 return $this->status; 558 } 559 // Check if the force import option is disabled and the last exported date column is present. 560 if (!$forceimport && !empty($timeexportkey)) { 561 $exportedtime = $line[$timeexportkey]; 562 if (clean_param($exportedtime, PARAM_INT) != $exportedtime || $exportedtime > time() || 563 $exportedtime < strtotime("-1 year", time())) { 564 // The date is invalid, or in the future, or more than a year old. 565 $this->cleanup_import(get_string('invalidgradeexporteddate', 'grades')); 566 return $this->status; 567 568 } 569 $timemodified = $gradegrade->get_dategraded(); 570 if (!empty($timemodified) && ($exportedtime < $timemodified)) { 571 // The item was graded after we exported it, we return here not to override it. 572 $user = core_user::get_user($this->studentid); 573 $this->cleanup_import(get_string('gradealreadyupdated', 'grades', fullname($user))); 574 return $this->status; 575 } 576 } 577 } 578 $insertid = self::insert_grade_record($newgrade, $this->studentid); 579 // Check to see if the insert was successful. 580 if (empty($insertid)) { 581 return null; 582 } 583 } 584 } 585 586 // Updating/inserting all comments here. 587 if ($this->status and !empty($this->newfeedbacks)) { 588 foreach ($this->newfeedbacks as $newfeedback) { 589 $sql = "SELECT * 590 FROM {grade_import_values} 591 WHERE importcode=? AND userid=? AND itemid=? AND importer=?"; 592 if ($feedback = $DB->get_record_sql($sql, array($this->importcode, $this->studentid, $newfeedback->itemid, 593 $USER->id))) { 594 $newfeedback->id = $feedback->id; 595 $DB->update_record('grade_import_values', $newfeedback); 596 597 } else { 598 // The grade item for this is not updated. 599 $newfeedback->importonlyfeedback = true; 600 $insertid = self::insert_grade_record($newfeedback, $this->studentid); 601 // Check to see if the insert was successful. 602 if (empty($insertid)) { 603 return null; 604 } 605 } 606 } 607 } 608 } 609 return $this->status; 610 } 611 612 /** 613 * Returns the headers parameter for this class. 614 * 615 * @return array returns headers parameter for this class. 616 */ 617 public function get_headers() { 618 return $this->headers; 619 } 620 621 /** 622 * Returns the error parameter for this class. 623 * 624 * @return string returns error parameter for this class. 625 */ 626 public function get_error() { 627 return $this->error; 628 } 629 630 /** 631 * Returns the iid parameter for this class. 632 * 633 * @return int returns iid parameter for this class. 634 */ 635 public function get_iid() { 636 return $this->iid; 637 } 638 639 /** 640 * Returns the preview_data parameter for this class. 641 * 642 * @return array returns previewdata parameter for this class. 643 */ 644 public function get_previewdata() { 645 return $this->previewdata; 646 } 647 648 /** 649 * Returns the gradebookerrors parameter for this class. 650 * 651 * @return array returns gradebookerrors parameter for this class. 652 */ 653 public function get_gradebookerrors() { 654 return $this->gradebookerrors; 655 } 656 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body