2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * A class for loading and preparing grade data from import.
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
25 defined('MOODLE_INTERNAL') ||
die();
28 * A class for loading and preparing grade data from import.
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
34 class gradeimport_csv_load_data
{
36 /** @var string $error csv import error. */
38 /** @var int $iid Unique identifier for these csv records. */
40 /** @var array $headers Column names for the data. */
42 /** @var array $previewdata A subsection of the csv imported data. */
43 protected $previewdata;
45 // The map_user_data_with_value variables.
46 /** @var array $newgrades Grades to be inserted into the gradebook. */
48 /** @var array $newfeedbacks Feedback to be inserted into the gradebook. */
49 protected $newfeedbacks;
50 /** @var int $studentid Student ID*/
53 // The prepare_import_grade_data() variables.
54 /** @var bool $status The current status of the import. True = okay, False = errors. */
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;
64 * Load CSV content for previewing.
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.
71 public function load_csv_content($text, $encoding, $separator, $previewrows) {
72 $this->raise_limits();
74 $this->iid
= csv_import_reader
::get_new_iid('grade');
75 $csvimport = new csv_import_reader($this->iid
, 'grade');
77 $csvimport->load_csv_content($text, $encoding, $separator);
78 $this->error
= $csvimport->get_error();
80 // If there are no import errors then proceed.
81 if (empty($this->error
)) {
83 // Get header (field names).
84 $this->headers
= $csvimport->get_columns();
85 $this->trim_headers();
88 $this->previewdata
= array();
90 for ($numlines = 0; $numlines <= $previewrows; $numlines++
) {
91 $lines = $csvimport->next();
93 $this->previewdata
[] = $lines;
100 * Gets all of the grade items in this course.
102 * @param int $courseid Course id;
103 * @return array An array of grade items for the course.
105 public static function fetch_grade_items($courseid) {
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') {
114 $displaystring = null;
115 if (!empty($gradeitem->itemmodule
)) {
116 $displaystring = get_string('modulename', $gradeitem->itemmodule
).get_string('labelsep', 'langconfig')
117 .$gradeitem->get_name();
119 $displaystring = $gradeitem->get_name();
121 $gradeitems[$gradeitem->id
] = $displaystring;
128 * Cleans the column headers from the CSV file.
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;
139 * Raises the php execution time and memory limits for importing the CSV file.
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
);
150 * Inserts a record into the grade_import_values table. This also adds common record information.
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.
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
;
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);
172 $this->cleanup_import(get_string('gradevaluetoobig', 'grades', $gradepointmaximum));
178 * Insert the new grade into the grade item buffer table.
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.
185 protected function import_new_grade_item($header, $key, $value) {
188 // First check if header is already in temp database.
189 if (empty($this->newgradeitems
[$key])) {
191 $newgradeitem = new stdClass();
192 $newgradeitem->itemname
= $header[$key];
193 $newgradeitem->importcode
= $this->importcode
;
194 $newgradeitem->importer
= $USER->id
;
196 // Insert into new grade item buffer.
197 $this->newgradeitems
[$key] = $DB->insert_record('grade_import_newitem', $newgradeitem);
199 $newgrade = new stdClass();
200 $newgrade->newgradeitem
= $this->newgradeitems
[$key];
202 $trimmed = trim($value);
203 if ($trimmed === '' or $trimmed == '-') {
204 // Blank or dash grade means null, ie "no grade".
205 $newgrade->finalgrade
= null;
207 // We have an actual grade.
208 $newgrade->finalgrade
= $value;
210 $this->newgrades
[] = $newgrade;
215 * Check that the user is in the system.
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.
221 protected function check_user_exists($value, $userfields) {
226 // The user may use the incorrect field to match the user. This could result in an exception.
228 $field = $userfields['field'];
229 // Fields that can be queried in a case-insensitive manner.
230 $caseinsensitivefields = [
234 // Build query predicate.
235 if (in_array($field, $caseinsensitivefields)) {
237 $select = $DB->sql_equal($field, ':' . $field, false);
240 $select = "{$field} = :{$field}";
243 // Make sure the record exists and that there's only one matching record found.
244 $user = $DB->get_record_select('user', $select, array($userfields['field'] => $value), '*', MUST_EXIST
);
245 } catch (dml_missing_record_exception
$missingex) {
246 $errorkey = 'usermappingerror';
247 } catch (dml_multiple_records_exception
$multiex) {
248 $errorkey = 'usermappingerrormultipleusersfound';
250 // Field may be fine, but no records were returned.
252 $usermappingerrorobj = new stdClass();
253 $usermappingerrorobj->field
= $userfields['label'];
254 $usermappingerrorobj->value
= $value;
255 $this->cleanup_import(get_string($errorkey, 'grades', $usermappingerrorobj));
256 unset($usermappingerrorobj);
263 * Check to see if the feedback matches a grade item.
265 * @param int $courseid The course ID.
266 * @param int $itemid The ID of the grade item that the feedback relates to.
267 * @param string $value The actual feedback being imported.
268 * @return object Creates a feedback object with the item ID and the feedback value.
270 protected function create_feedback($courseid, $itemid, $value) {
271 // Case of an id, only maps id of a grade_item.
272 // This was idnumber.
273 if (!new grade_item(array('id' => $itemid, 'courseid' => $courseid))) {
274 // Supplied bad mapping, should not be possible since user
275 // had to pick mapping.
276 $this->cleanup_import(get_string('importfailed', 'grades'));
280 // The itemid is the id of the grade item.
281 $feedback = new stdClass();
282 $feedback->itemid
= $itemid;
283 $feedback->feedback
= $value;
288 * This updates existing grade items.
290 * @param int $courseid The course ID.
291 * @param array $map Mapping information provided by the user.
292 * @param int $key The line that we are currently working on.
293 * @param bool $verbosescales Form setting for grading with scales.
294 * @param string $value The grade value.
295 * @return array grades to be updated.
297 protected function update_grade_item($courseid, $map, $key, $verbosescales, $value) {
298 // Case of an id, only maps id of a grade_item.
299 // This was idnumber.
300 if (!$gradeitem = new grade_item(array('id' => $map[$key], 'courseid' => $courseid))) {
301 // Supplied bad mapping, should not be possible since user
302 // had to pick mapping.
303 $this->cleanup_import(get_string('importfailed', 'grades'));
307 // Check if grade item is locked if so, abort.
308 if ($gradeitem->is_locked()) {
309 $this->cleanup_import(get_string('gradeitemlocked', 'grades'));
313 $newgrade = new stdClass();
314 $newgrade->itemid
= $gradeitem->id
;
315 if ($gradeitem->gradetype
== GRADE_TYPE_SCALE
and $verbosescales) {
316 if ($value === '' or $value == '-') {
317 $value = null; // No grade.
319 $scale = $gradeitem->load_scale();
320 $scales = explode(',', $scale->scale
);
321 $scales = array_map('trim', $scales); // Hack - trim whitespace around scale options.
322 array_unshift($scales, '-'); // Scales start at key 1.
323 $key = array_search($value, $scales);
324 if ($key === false) {
325 $this->cleanup_import(get_string('badgrade', 'grades'));
330 $newgrade->finalgrade
= $value;
332 if ($value === '' or $value == '-') {
333 $value = null; // No grade.
335 // If the value has a local decimal or can correctly be unformatted, do it.
336 $validvalue = unformat_float($value, true);
337 if ($validvalue !== false) {
338 $value = $validvalue;
340 // Non numeric grade value supplied, possibly mapped wrong column.
341 $this->cleanup_import(get_string('badgrade', 'grades'));
345 $newgrade->finalgrade
= $value;
347 $this->newgrades
[] = $newgrade;
348 return $this->newgrades
;
352 * Clean up failed CSV grade import. Clears the temp table for inserting grades.
354 * @param string $notification The error message to display from the unsuccessful grade import.
356 protected function cleanup_import($notification) {
357 $this->status
= false;
358 import_cleanup($this->importcode
);
359 $this->gradebookerrors
[] = $notification;
363 * Check user mapping.
365 * @param string $mappingidentifier The user field that we are matching together.
366 * @param string $value The value we are checking / importing.
367 * @param array $header The column headers of the csv file.
368 * @param array $map Mapping information provided by the user.
369 * @param int $key Current row identifier.
370 * @param int $courseid The course ID.
371 * @param int $feedbackgradeid The ID of the grade item that the feedback relates to.
372 * @param bool $verbosescales Form setting for grading with scales.
374 protected function map_user_data_with_value($mappingidentifier, $value, $header, $map, $key, $courseid, $feedbackgradeid,
377 // Fields that the user can be mapped from.
383 'useridnumber' => array(
384 'field' => 'idnumber',
385 'label' => 'idnumber',
387 'useremail' => array(
389 'label' => 'email address',
392 'field' => 'username',
393 'label' => 'username',
397 switch ($mappingidentifier) {
402 $this->studentid
= $this->check_user_exists($value, $userfields[$mappingidentifier]);
405 $this->import_new_grade_item($header, $key, $value);
408 if ($feedbackgradeid) {
409 $feedback = $this->create_feedback($courseid, $feedbackgradeid, $value);
410 if (isset($feedback)) {
411 $this->newfeedbacks
[] = $feedback;
416 // Existing grade items.
417 if (!empty($map[$key])) {
418 $this->newgrades
= $this->update_grade_item($courseid, $map, $key, $verbosescales, $value,
421 // Otherwise, we ignore this column altogether because user has chosen
422 // to ignore them (e.g. institution, address etc).
428 * Checks and prepares grade data for inserting into the gradebook.
430 * @param array $header Column headers of the CSV file.
431 * @param object $formdata Mapping information from the preview page.
432 * @param object $csvimport csv import reader object for iterating over the imported CSV file.
433 * @param int $courseid The course ID.
434 * @param bool $separatemode If we have groups are they separate?
435 * @param mixed $currentgroup current group information.
436 * @param bool $verbosescales Form setting for grading with scales.
437 * @return bool True if the status for importing is okay, false if there are errors.
439 public function prepare_import_grade_data($header, $formdata, $csvimport, $courseid, $separatemode, $currentgroup,
443 // The import code is used for inserting data into the grade tables.
444 $this->importcode
= $formdata->importcode
;
445 $this->status
= true;
446 $this->headers
= $header;
447 $this->studentid
= null;
448 $this->gradebookerrors
= null;
449 $forceimport = $formdata->forceimport
;
450 // Temporary array to keep track of what new headers are processed.
451 $this->newgradeitems
= array();
452 $this->trim_headers();
453 $timeexportkey = null;
455 // Loops mapping_0, mapping_1 .. mapping_n and construct $map array.
456 foreach ($header as $i => $head) {
457 if (isset($formdata->{'mapping_'.$i})) {
458 $map[$i] = $formdata->{'mapping_'.$i};
460 if ($head == get_string('timeexported', 'gradeexport_txt')) {
465 // If mapping information is supplied.
466 $map[clean_param($formdata->mapfrom
, PARAM_RAW
)] = clean_param($formdata->mapto
, PARAM_RAW
);
468 // Check for mapto collisions.
469 $maperrors = array();
470 foreach ($map as $i => $j) {
472 // You can have multiple ignores.
475 if (!isset($maperrors[$j])) {
476 $maperrors[$j] = true;
479 print_error('cannotmapfield', '', '', $j);
484 $this->raise_limits();
488 while ($line = $csvimport->next()) {
489 if (count($line) <= 1) {
490 // There is no data on this line, move on.
494 // Array to hold all grades to be inserted.
495 $this->newgrades
= array();
496 // Array to hold all feedback.
497 $this->newfeedbacks
= array();
498 // Each line is a student record.
499 foreach ($line as $key => $value) {
501 $value = clean_param($value, PARAM_RAW
);
502 $value = trim($value);
506 * 1) userid, useridnumber, usermail, username - used to identify user row
507 * 2) new - new grade item
508 * 3) id - id of the old grade item to map onto
509 * 3) feedback_id - feedback for grade item id
512 // Explode the mapping for feedback into a label 'feedback' and the identifying number.
513 $mappingbase = explode("_", $map[$key]);
514 $mappingidentifier = $mappingbase[0];
515 // Set the feedback identifier if it exists.
516 if (isset($mappingbase[1])) {
517 $feedbackgradeid = (int)$mappingbase[1];
519 $feedbackgradeid = '';
522 $this->map_user_data_with_value($mappingidentifier, $value, $header, $map, $key, $courseid, $feedbackgradeid,
524 if ($this->status
=== false) {
525 return $this->status
;
529 // No user mapping supplied at all, or user mapping failed.
530 if (empty($this->studentid
) ||
!is_numeric($this->studentid
)) {
531 // User not found, abort whole import.
532 $this->cleanup_import(get_string('usermappingerrorusernotfound', 'grades'));
536 if ($separatemode and !groups_is_member($currentgroup, $this->studentid
)) {
537 // Not allowed to import into this group, abort.
538 $this->cleanup_import(get_string('usermappingerrorcurrentgroup', 'grades'));
542 // Insert results of this students into buffer.
543 if ($this->status
and !empty($this->newgrades
)) {
545 foreach ($this->newgrades
as $newgrade) {
547 // Check if grade_grade is locked and if so, abort.
548 if (!empty($newgrade->itemid
) and $gradegrade = new grade_grade(array('itemid' => $newgrade->itemid
,
549 'userid' => $this->studentid
))) {
550 if ($gradegrade->is_locked()) {
551 // Individual grade locked.
552 $this->cleanup_import(get_string('gradelocked', 'grades'));
553 return $this->status
;
555 // Check if the force import option is disabled and the last exported date column is present.
556 if (!$forceimport && !empty($timeexportkey)) {
557 $exportedtime = $line[$timeexportkey];
558 if (clean_param($exportedtime, PARAM_INT
) != $exportedtime ||
$exportedtime > time() ||
559 $exportedtime < strtotime("-1 year", time())) {
560 // The date is invalid, or in the future, or more than a year old.
561 $this->cleanup_import(get_string('invalidgradeexporteddate', 'grades'));
562 return $this->status
;
565 $timemodified = $gradegrade->get_dategraded();
566 if (!empty($timemodified) && ($exportedtime < $timemodified)) {
567 // The item was graded after we exported it, we return here not to override it.
568 $user = core_user
::get_user($this->studentid
);
569 $this->cleanup_import(get_string('gradealreadyupdated', 'grades', fullname($user)));
570 return $this->status
;
574 $insertid = self
::insert_grade_record($newgrade, $this->studentid
);
575 // Check to see if the insert was successful.
576 if (empty($insertid)) {
582 // Updating/inserting all comments here.
583 if ($this->status
and !empty($this->newfeedbacks
)) {
584 foreach ($this->newfeedbacks
as $newfeedback) {
586 FROM {grade_import_values}
587 WHERE importcode=? AND userid=? AND itemid=? AND importer=?";
588 if ($feedback = $DB->get_record_sql($sql, array($this->importcode
, $this->studentid
, $newfeedback->itemid
,
590 $newfeedback->id
= $feedback->id
;
591 $DB->update_record('grade_import_values', $newfeedback);
594 // The grade item for this is not updated.
595 $newfeedback->importonlyfeedback
= true;
596 $insertid = self
::insert_grade_record($newfeedback, $this->studentid
);
597 // Check to see if the insert was successful.
598 if (empty($insertid)) {
605 return $this->status
;
609 * Returns the headers parameter for this class.
611 * @return array returns headers parameter for this class.
613 public function get_headers() {
614 return $this->headers
;
618 * Returns the error parameter for this class.
620 * @return string returns error parameter for this class.
622 public function get_error() {
627 * Returns the iid parameter for this class.
629 * @return int returns iid parameter for this class.
631 public function get_iid() {
636 * Returns the preview_data parameter for this class.
638 * @return array returns previewdata parameter for this class.
640 public function get_previewdata() {
641 return $this->previewdata
;
645 * Returns the gradebookerrors parameter for this class.
647 * @return array returns gradebookerrors parameter for this class.
649 public function get_gradebookerrors() {
650 return $this->gradebookerrors
;