56b27b9b96a5651a42dfd2fb2f553b34d755a2e5
[moodle.git] / grade / import / csv / index.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 require_once '../../../config.php';
19 require_once $CFG->libdir.'/gradelib.php';
20 require_once $CFG->dirroot.'/grade/lib.php';
21 require_once '../grade_import_form.php';
22 require_once '../lib.php';
24 $id            = required_param('id', PARAM_INT); // course id
25 $separator     = optional_param('separator', '', PARAM_ALPHA);
26 $verbosescales = optional_param('verbosescales', 1, PARAM_BOOL);
28 $url = new moodle_url('/grade/import/csv/index.php', array('id'=>$id));
29 if ($separator !== '') {
30     $url->param('separator', $separator);
31 }
32 if ($verbosescales !== 1) {
33     $url->param('verbosescales', $verbosescales);
34 }
35 $PAGE->set_url($url);
37 define('GRADE_CSV_LINE_LENGTH', 4096);
39 if (!$course = $DB->get_record('course', array('id'=>$id))) {
40     print_error('nocourseid');
41 }
43 require_login($course);
44 $context = get_context_instance(CONTEXT_COURSE, $id);
45 require_capability('moodle/grade:import', $context);
46 require_capability('gradeimport/csv:view', $context);
48 $separatemode = (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context));
49 $currentgroup = groups_get_course_group($course);
51 // sort out delimiter
52 if (isset($CFG->CSV_DELIMITER)) {
53     $csv_delimiter = $CFG->CSV_DELIMITER;
55     if (isset($CFG->CSV_ENCODE)) {
56         $csv_encode = '/\&\#' . $CFG->CSV_ENCODE . '/';
57     }
58 } else if ($separator == 'tab') {
59     $csv_delimiter = "\t";
60     $csv_encode = "";
61 } else {
62     $csv_delimiter = ",";
63     $csv_encode = '/\&\#44/';
64 }
66 print_grade_page_head($course->id, 'import', 'csv', get_string('importcsv', 'grades'));
68 // set up import form
69 $mform = new grade_import_form(null, array('includeseparator'=>!isset($CFG->CSV_DELIMITER), 'verbosescales'=>true));
71 // set up grade import mapping form
72 $header = '';
73 $gradeitems = array();
74 if ($id) {
75     if ($grade_items = grade_item::fetch_all(array('courseid'=>$id))) {
76         foreach ($grade_items as $grade_item) {
77             // skip course type and category type
78             if ($grade_item->itemtype == 'course' || $grade_item->itemtype == 'category') {
79                 continue;
80             }
82             // this was idnumber
83             $gradeitems[$grade_item->id] = $grade_item->get_name();
84         }
85     }
86 }
88 if ($importcode = optional_param('importcode', '', PARAM_FILE)) {
89     $filename = $CFG->dataroot.'/temp/gradeimport/cvs/'.$USER->id.'/'.$importcode;
90     $fp = fopen($filename, "r");
91     $header = explode($csv_delimiter, fgets($fp,GRADE_CSV_LINE_LENGTH), PARAM_RAW);
92 }
94 $mform2 = new grade_import_mapping_form(null, array('gradeitems'=>$gradeitems, 'header'=>$header));
96 // if import form is submitted
97 if ($formdata = $mform->get_data()) {
99     // Large files are likely to take their time and memory. Let PHP know
100     // that we'll take longer, and that the process should be recycled soon
101     // to free up memory.
102     @set_time_limit(0);
103     @raise_memory_limit("192M");
104     if (function_exists('apache_child_terminate')) {
105         @apache_child_terminate();
106     }
108     // use current (non-conflicting) time stamp
109     $importcode = get_new_importcode();
110     $filename = make_upload_directory('temp/gradeimport/cvs/'.$USER->id);
111     $filename = $filename.'/'.$importcode;
113     $text = $mform->get_file_content('userfile');
114     // trim utf-8 bom
115     $textlib = textlib_get_instance();
116     /// normalize line endings and do the encoding conversion
117     $text = $textlib->convert($text, $formdata->encoding);
118     $text = $textlib->trim_utf8_bom($text);
119     // Fix mac/dos newlines
120     $text = preg_replace('!\r\n?!',"\n",$text);
121     $fp = fopen($filename, "w");
122     fwrite($fp,$text);
123     fclose($fp);
125     $fp = fopen($filename, "r");
127     // --- get header (field names) ---
128     $header = explode($csv_delimiter, fgets($fp,GRADE_CSV_LINE_LENGTH));
130     // print some preview
131     $numlines = 0; // 0 preview lines displayed
133     echo $OUTPUT->heading(get_string('importpreview', 'grades'));
134     echo '<table>';
135     echo '<tr>';
136     foreach ($header as $h) {
137         $h = clean_param($h, PARAM_RAW);
138         echo '<th>'.$h.'</th>';
139     }
140     echo '</tr>';
141     while (!feof ($fp) && $numlines <= $formdata->previewrows) {
142         $lines = explode($csv_delimiter, fgets($fp,GRADE_CSV_LINE_LENGTH));
143         echo '<tr>';
144         foreach ($lines as $line) {
145             echo '<td>'.$line.'</td>';;
146         }
147         $numlines ++;
148         echo '</tr>';
149     }
150     echo '</table>';
152     // display the mapping form with header info processed
153     $mform2 = new grade_import_mapping_form(null, array('gradeitems'=>$gradeitems, 'header'=>$header));
154     $mform2->set_data(array('importcode'=>$importcode, 'id'=>$id, 'verbosescales'=>$verbosescales, 'separator'=>$separator));
155     $mform2->display();
157 //} else if (($formdata = data_submitted()) && !empty($formdata->map)) {
159 // else if grade import mapping form is submitted
160 } else if ($formdata = $mform2->get_data()) {
162     $importcode = clean_param($formdata->importcode, PARAM_FILE);
163     $filename = $CFG->dataroot.'/temp/gradeimport/cvs/'.$USER->id.'/'.$importcode;
165     if (!file_exists($filename)) {
166         print_error('cannotuploadfile');
167     }
169     if ($fp = fopen($filename, "r")) {
170         // --- get header (field names) ---
171         $header = explode($csv_delimiter, clean_param(fgets($fp,GRADE_CSV_LINE_LENGTH), PARAM_RAW));
173         foreach ($header as $i => $h) {
174             $h = trim($h); $header[$i] = $h; // remove whitespace
175         }
176     } else {
177         print_error('cannotopenfile');
178     }
180     $map = array();
181     // loops mapping_0, mapping_1 .. mapping_n and construct $map array
182     foreach ($header as $i => $head) {
183         $map[$i] = $formdata->{'mapping_'.$i};
184     }
186     // if mapping information is supplied
187     $map[clean_param($formdata->mapfrom, PARAM_RAW)] = clean_param($formdata->mapto, PARAM_RAW);
189     // check for mapto collisions
190     $maperrors = array();
191     foreach ($map as $i=>$j) {
192         if ($j == 0) {
193             // you can have multiple ignores
194             continue;
195         } else {
196             if (!isset($maperrors[$j])) {
197                 $maperrors[$j] = true;
198             } else {
199                 // collision
200                 fclose($fp);
201                 unlink($filename); // needs to be uploaded again, sorry
202                 print_error('cannotmapfield', '', '', $j);
203             }
204         }
205     }
207     // Large files are likely to take their time and memory. Let PHP know
208     // that we'll take longer, and that the process should be recycled soon
209     // to free up memory.
210     @set_time_limit(0);
211     @raise_memory_limit("192M");
212     if (function_exists('apache_child_terminate')) {
213         @apache_child_terminate();
214     }
216     // we only operate if file is readable
217     if ($fp = fopen($filename, "r")) {
219         // read the first line makes sure this doesn't get read again
220         $header = explode($csv_delimiter, fgets($fp,GRADE_CSV_LINE_LENGTH));
222         $newgradeitems = array(); // temporary array to keep track of what new headers are processed
223         $status = true;
225         while (!feof ($fp)) {
226             // add something
227             $line = explode($csv_delimiter, fgets($fp,GRADE_CSV_LINE_LENGTH));
229             if(count($line) <= 1){
230                 // there is no data on this line, move on
231                 continue;
232             }
234             // array to hold all grades to be inserted
235             $newgrades = array();
236             // array to hold all feedback
237             $newfeedbacks = array();
238             // each line is a student record
239             foreach ($line as $key => $value) {
240                 //decode encoded commas
241                 $value = clean_param($value, PARAM_RAW);
242                 $value = trim($value);
243                 if (!empty($csv_encode)) {
244                     $value = preg_replace($csv_encode, $csv_delimiter, $value);
245                 }
247                 /*
248                  * the options are
249                  * 1) userid, useridnumber, usermail, username - used to identify user row
250                  * 2) new - new grade item
251                  * 3) id - id of the old grade item to map onto
252                  * 3) feedback_id - feedback for grade item id
253                  */
255                 $t = explode("_", $map[$key]);
256                 $t0 = $t[0];
257                 if (isset($t[1])) {
258                     $t1 = (int)$t[1];
259                 } else {
260                     $t1 = '';
261                 }
263                 switch ($t0) {
264                     case 'userid': //
265                         if (!$user = $DB->get_record('user', array('id' => $value))) {
266                             // user not found, abort whole import
267                             import_cleanup($importcode);
268                             echo $OUTPUT->notification("user mapping error, could not find user with id \"$value\"");
269                             $status = false;
270                             break 3;
271                         }
272                         $studentid = $value;
273                     break;
274                     case 'useridnumber':
275                         if (!$user = $DB->get_record('user', array('idnumber' => $value))) {
276                              // user not found, abort whole import
277                             import_cleanup($importcode);
278                             echo $OUTPUT->notification("user mapping error, could not find user with idnumber \"$value\"");
279                             $status = false;
280                             break 3;
281                         }
282                         $studentid = $user->id;
283                     break;
284                     case 'useremail':
285                         if (!$user = $DB->get_record('user', array('email' => $value))) {
286                             import_cleanup($importcode);
287                             echo $OUTPUT->notification("user mapping error, could not find user with email address \"$value\"");
288                             $status = false;
289                             break 3;
290                         }
291                         $studentid = $user->id;
292                     break;
293                     case 'username':
294                         if (!$user = $DB->get_record('user', array('username' => $value))) {
295                             import_cleanup($importcode);
296                             echo $OUTPUT->notification("user mapping error, could not find user with username \"$value\"");
297                             $status = false;
298                             break 3;
299                         }
300                         $studentid = $user->id;
301                     break;
302                     case 'new':
303                         // first check if header is already in temp database
305                         if (empty($newgradeitems[$key])) {
307                             $newgradeitem = new stdClass();
308                             $newgradeitem->itemname = $header[$key];
309                             $newgradeitem->importcode = $importcode;
310                             $newgradeitem->importer   = $USER->id;
312                             // failed to insert into new grade item buffer
313                             $newgradeitems[$key] = $DB->insert_record('grade_import_newitem', $newgradeitem);
314                             // add this to grade_import_newitem table
315                             // add the new id to $newgradeitem[$key]
316                         }
317                         $newgrade = new stdClass();
318                         $newgrade->newgradeitem = $newgradeitems[$key];
319                         $newgrade->finalgrade   = $value;
320                         $newgrades[] = $newgrade;
322                         // if not, put it in
323                         // else, insert grade into the table
324                     break;
325                     case 'feedback':
326                         if ($t1) {
327                             // case of an id, only maps id of a grade_item
328                             // this was idnumber
329                             if (!$gradeitem = new grade_item(array('id'=>$t1, 'courseid'=>$course->id))) {
330                                 // supplied bad mapping, should not be possible since user
331                                 // had to pick mapping
332                                 $status = false;
333                                 import_cleanup($importcode);
334                                 echo $OUTPUT->notification(get_string('importfailed', 'grades'));
335                                 break 3;
336                             }
338                             // t1 is the id of the grade item
339                             $feedback = new stdClass();
340                             $feedback->itemid   = $t1;
341                             $feedback->feedback = $value;
342                             $newfeedbacks[] = $feedback;
343                         }
344                     break;
345                     default:
346                         // existing grade items
347                         if (!empty($map[$key])) {
348                             // case of an id, only maps id of a grade_item
349                             // this was idnumber
350                             if (!$gradeitem = new grade_item(array('id'=>$map[$key], 'courseid'=>$course->id))) {
351                                 // supplied bad mapping, should not be possible since user
352                                 // had to pick mapping
353                                 $status = false;
354                                 import_cleanup($importcode);
355                                 echo $OUTPUT->notification(get_string('importfailed', 'grades'));
356                                 break 3;
357                             }
359                             // check if grade item is locked if so, abort
360                             if ($gradeitem->is_locked()) {
361                                 $status = false;
362                                 import_cleanup($importcode);
363                                 echo $OUTPUT->notification(get_string('gradeitemlocked', 'grades'));
364                                 break 3;
365                             }
367                             $newgrade = new stdClass();
368                             $newgrade->itemid     = $gradeitem->id;
369                             if ($gradeitem->gradetype == GRADE_TYPE_SCALE and $verbosescales) {
370                                 if ($value === '' or $value == '-') {
371                                     $value = null; // no grade
372                                 } else {
373                                     $scale = $gradeitem->load_scale();
374                                     $scales = explode(',', $scale->scale);
375                                     $scales = array_map('trim', $scales); //hack - trim whitespace around scale options
376                                     array_unshift($scales, '-'); // scales start at key 1
377                                     $key = array_search($value, $scales);
378                                     if ($key === false) {
379                                         echo "<br/>t0 is $t0";
380                                         echo "<br/>grade is $value";
381                                         $status = false;
382                                         import_cleanup($importcode);
383                                         echo $OUTPUT->notification(get_string('badgrade', 'grades'));
384                                         break 3;
385                                     }
386                                     $value = $key;
387                                 }
388                                 $newgrade->finalgrade = $value;
389                             } else {
390                                 if ($value === '' or $value == '-') {
391                                     $value = null; // no grade
393                                 } else if (!is_numeric($value)) {
394                                 // non numeric grade value supplied, possibly mapped wrong column
395                                     echo "<br/>t0 is $t0";
396                                     echo "<br/>grade is $value";
397                                     $status = false;
398                                     import_cleanup($importcode);
399                                     echo $OUTPUT->notification(get_string('badgrade', 'grades'));
400                                     break 3;
401                                 }
402                                 $newgrade->finalgrade = $value;
403                             }
404                             $newgrades[] = $newgrade;
405                         } // otherwise, we ignore this column altogether
406                           // because user has chosen to ignore them (e.g. institution, address etc)
407                     break;
408                 }
409             }
411             // no user mapping supplied at all, or user mapping failed
412             if (empty($studentid) || !is_numeric($studentid)) {
413                 // user not found, abort whole import
414                 $status = false;
415                 import_cleanup($importcode);
416                 echo $OUTPUT->notification('user mapping error, could not find user!');
417                 break;
418             }
420             if ($separatemode and !groups_is_member($currentgroup, $studentid)) {
421                 // not allowed to import into this group, abort
422                 $status = false;
423                 import_cleanup($importcode);
424                 echo $OUTPUT->notification('user not member of current group, can not update!');
425                 break;
426             }
428             // insert results of this students into buffer
429             if ($status and !empty($newgrades)) {
431                 foreach ($newgrades as $newgrade) {
433                     // check if grade_grade is locked and if so, abort
434                     if (!empty($newgrade->itemid) and $grade_grade = new grade_grade(array('itemid'=>$newgrade->itemid, 'userid'=>$studentid))) {
435                         if ($grade_grade->is_locked()) {
436                             // individual grade locked
437                             $status = false;
438                             import_cleanup($importcode);
439                             echo $OUTPUT->notification(get_string('gradelocked', 'grades'));
440                             break 2;
441                         }
442                     }
444                     $newgrade->importcode = $importcode;
445                     $newgrade->userid     = $studentid;
446                     $newgrade->importer   = $USER->id;
447                     $DB->insert_record('grade_import_values', $newgrade);
448                 }
449             }
451             // updating/inserting all comments here
452             if ($status and !empty($newfeedbacks)) {
453                 foreach ($newfeedbacks as $newfeedback) {
454                     $sql = "SELECT *
455                               FROM {grade_import_values}
456                              WHERE importcode=? AND userid=? AND itemid=? AND importer=?";
457                     if ($feedback = $DB->get_record_sql($sql, array($importcode, $studentid, $newfeedback->itemid, $USER->id))) {
458                         $newfeedback->id = $feedback->id;
459                         $DB->update_record('grade_import_values', $newfeedback);
461                     } else {
462                         // the grade item for this is not updated
463                         $newfeedback->importcode = $importcode;
464                         $newfeedback->userid     = $studentid;
465                         $newfeedback->importer   = $USER->id;
466                         $DB->insert_record('grade_import_values', $newfeedback);
467                     }
468                 }
469             }
470         }
472         /// at this stage if things are all ok, we commit the changes from temp table
473         if ($status) {
474             grade_import_commit($course->id, $importcode);
475         }
476         // temporary file can go now
477         fclose($fp);
478         unlink($filename);
479     } else {
480         print_error('cannotreadfil');
481     }
483 } else {
484     groups_print_course_menu($course, 'index.php?id='.$id);
485     echo '<div class="clearer"></div>';
487     // display the standard upload file form
488     $mform->display();
491 echo $OUTPUT->footer();