0201bb3cc65301b029dc07c4d913fd117a4c8976
[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     $headers = fgets($fp,GRADE_CSV_LINE_LENGTH);
92     $header = explode($csv_delimiter, $headers);
93 }
95 $mform2 = new grade_import_mapping_form(null, array('gradeitems'=>$gradeitems, 'header'=>$header));
97 // if import form is submitted
98 if ($formdata = $mform->get_data()) {
100     // Large files are likely to take their time and memory. Let PHP know
101     // that we'll take longer, and that the process should be recycled soon
102     // to free up memory.
103     @set_time_limit(0);
104     raise_memory_limit(MEMORY_EXTRA);
106     // use current (non-conflicting) time stamp
107     $importcode = get_new_importcode();
108     $filename = make_upload_directory('temp/gradeimport/cvs/'.$USER->id);
109     $filename = $filename.'/'.$importcode;
111     $text = $mform->get_file_content('userfile');
112     // trim utf-8 bom
113     $textlib = textlib_get_instance();
114     /// normalize line endings and do the encoding conversion
115     $text = $textlib->convert($text, $formdata->encoding);
116     $text = $textlib->trim_utf8_bom($text);
117     // Fix mac/dos newlines
118     $text = preg_replace('!\r\n?!',"\n",$text);
119     $fp = fopen($filename, "w");
120     fwrite($fp,$text);
121     fclose($fp);
123     $fp = fopen($filename, "r");
125     // --- get header (field names) ---
126     $header = explode($csv_delimiter, fgets($fp,GRADE_CSV_LINE_LENGTH));
128     // print some preview
129     $numlines = 0; // 0 preview lines displayed
131     echo $OUTPUT->heading(get_string('importpreview', 'grades'));
132     echo '<table>';
133     echo '<tr>';
134     foreach ($header as $h) {
135         $h = clean_param($h, PARAM_RAW);
136         echo '<th>'.$h.'</th>';
137     }
138     echo '</tr>';
139     while (!feof ($fp) && $numlines <= $formdata->previewrows) {
140         $lines = explode($csv_delimiter, fgets($fp,GRADE_CSV_LINE_LENGTH));
141         echo '<tr>';
142         foreach ($lines as $line) {
143             echo '<td>'.$line.'</td>';;
144         }
145         $numlines ++;
146         echo '</tr>';
147     }
148     echo '</table>';
150     // display the mapping form with header info processed
151     $mform2 = new grade_import_mapping_form(null, array('gradeitems'=>$gradeitems, 'header'=>$header));
152     $mform2->set_data(array('importcode'=>$importcode, 'id'=>$id, 'verbosescales'=>$verbosescales, 'separator'=>$separator));
153     $mform2->display();
155 //} else if (($formdata = data_submitted()) && !empty($formdata->map)) {
157 // else if grade import mapping form is submitted
158 } else if ($formdata = $mform2->get_data()) {
160     $importcode = clean_param($formdata->importcode, PARAM_FILE);
161     $filename = $CFG->dataroot.'/temp/gradeimport/cvs/'.$USER->id.'/'.$importcode;
163     if (!file_exists($filename)) {
164         print_error('cannotuploadfile');
165     }
167     if ($fp = fopen($filename, "r")) {
168         // --- get header (field names) ---
169         $header = explode($csv_delimiter, clean_param(fgets($fp,GRADE_CSV_LINE_LENGTH), PARAM_RAW));
171         foreach ($header as $i => $h) {
172             $h = trim($h); $header[$i] = $h; // remove whitespace
173         }
174     } else {
175         print_error('cannotopenfile');
176     }
178     $map = array();
179     // loops mapping_0, mapping_1 .. mapping_n and construct $map array
180     foreach ($header as $i => $head) {
181         if (isset($formdata->{'mapping_'.$i})) {
182             $map[$i] = $formdata->{'mapping_'.$i};
183         }
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(MEMORY_EXTRA);
213     // we only operate if file is readable
214     if ($fp = fopen($filename, "r")) {
216         // read the first line makes sure this doesn't get read again
217         $header = explode($csv_delimiter, fgets($fp,GRADE_CSV_LINE_LENGTH));
219         $newgradeitems = array(); // temporary array to keep track of what new headers are processed
220         $status = true;
222         while (!feof ($fp)) {
223             // add something
224             $line = explode($csv_delimiter, fgets($fp,GRADE_CSV_LINE_LENGTH));
226             if(count($line) <= 1){
227                 // there is no data on this line, move on
228                 continue;
229             }
231             // array to hold all grades to be inserted
232             $newgrades = array();
233             // array to hold all feedback
234             $newfeedbacks = array();
235             // each line is a student record
236             foreach ($line as $key => $value) {
237                 //decode encoded commas
238                 $value = clean_param($value, PARAM_RAW);
239                 $value = trim($value);
240                 if (!empty($csv_encode)) {
241                     $value = preg_replace($csv_encode, $csv_delimiter, $value);
242                 }
244                 /*
245                  * the options are
246                  * 1) userid, useridnumber, usermail, username - used to identify user row
247                  * 2) new - new grade item
248                  * 3) id - id of the old grade item to map onto
249                  * 3) feedback_id - feedback for grade item id
250                  */
252                 $t = explode("_", $map[$key]);
253                 $t0 = $t[0];
254                 if (isset($t[1])) {
255                     $t1 = (int)$t[1];
256                 } else {
257                     $t1 = '';
258                 }
260                 switch ($t0) {
261                     case 'userid': //
262                         if (!$user = $DB->get_record('user', array('id' => $value))) {
263                             // user not found, abort whole import
264                             import_cleanup($importcode);
265                             echo $OUTPUT->notification("user mapping error, could not find user with id \"$value\"");
266                             $status = false;
267                             break 3;
268                         }
269                         $studentid = $value;
270                     break;
271                     case 'useridnumber':
272                         if (!$user = $DB->get_record('user', array('idnumber' => $value))) {
273                              // user not found, abort whole import
274                             import_cleanup($importcode);
275                             echo $OUTPUT->notification("user mapping error, could not find user with idnumber \"$value\"");
276                             $status = false;
277                             break 3;
278                         }
279                         $studentid = $user->id;
280                     break;
281                     case 'useremail':
282                         if (!$user = $DB->get_record('user', array('email' => $value))) {
283                             import_cleanup($importcode);
284                             echo $OUTPUT->notification("user mapping error, could not find user with email address \"$value\"");
285                             $status = false;
286                             break 3;
287                         }
288                         $studentid = $user->id;
289                     break;
290                     case 'username':
291                         if (!$user = $DB->get_record('user', array('username' => $value))) {
292                             import_cleanup($importcode);
293                             echo $OUTPUT->notification("user mapping error, could not find user with username \"$value\"");
294                             $status = false;
295                             break 3;
296                         }
297                         $studentid = $user->id;
298                     break;
299                     case 'new':
300                         // first check if header is already in temp database
302                         if (empty($newgradeitems[$key])) {
304                             $newgradeitem = new stdClass();
305                             $newgradeitem->itemname = $header[$key];
306                             $newgradeitem->importcode = $importcode;
307                             $newgradeitem->importer   = $USER->id;
309                             // insert into new grade item buffer
310                             $newgradeitems[$key] = $DB->insert_record('grade_import_newitem', $newgradeitem);
311                         }
312                         $newgrade = new stdClass();
313                         $newgrade->newgradeitem = $newgradeitems[$key];
314                         
315                         //if the user has a grade for this grade item
316                         if (trim($value)!='-') {
317                             //instead of omitting the grade we could insert one with finalgrade set to 0
318                             //we do not have access to grade item min grade
319                             $newgrade->finalgrade   = $value;
320                             $newgrades[] = $newgrade;
321                         }
322                     break;
323                     case 'feedback':
324                         if ($t1) {
325                             // case of an id, only maps id of a grade_item
326                             // this was idnumber
327                             if (!$gradeitem = new grade_item(array('id'=>$t1, 'courseid'=>$course->id))) {
328                                 // supplied bad mapping, should not be possible since user
329                                 // had to pick mapping
330                                 $status = false;
331                                 import_cleanup($importcode);
332                                 echo $OUTPUT->notification(get_string('importfailed', 'grades'));
333                                 break 3;
334                             }
336                             // t1 is the id of the grade item
337                             $feedback = new stdClass();
338                             $feedback->itemid   = $t1;
339                             $feedback->feedback = $value;
340                             $newfeedbacks[] = $feedback;
341                         }
342                     break;
343                     default:
344                         // existing grade items
345                         if (!empty($map[$key])) {
346                             // case of an id, only maps id of a grade_item
347                             // this was idnumber
348                             if (!$gradeitem = new grade_item(array('id'=>$map[$key], 'courseid'=>$course->id))) {
349                                 // supplied bad mapping, should not be possible since user
350                                 // had to pick mapping
351                                 $status = false;
352                                 import_cleanup($importcode);
353                                 echo $OUTPUT->notification(get_string('importfailed', 'grades'));
354                                 break 3;
355                             }
357                             // check if grade item is locked if so, abort
358                             if ($gradeitem->is_locked()) {
359                                 $status = false;
360                                 import_cleanup($importcode);
361                                 echo $OUTPUT->notification(get_string('gradeitemlocked', 'grades'));
362                                 break 3;
363                             }
365                             $newgrade = new stdClass();
366                             $newgrade->itemid     = $gradeitem->id;
367                             if ($gradeitem->gradetype == GRADE_TYPE_SCALE and $verbosescales) {
368                                 if ($value === '' or $value == '-') {
369                                     $value = null; // no grade
370                                 } else {
371                                     $scale = $gradeitem->load_scale();
372                                     $scales = explode(',', $scale->scale);
373                                     $scales = array_map('trim', $scales); //hack - trim whitespace around scale options
374                                     array_unshift($scales, '-'); // scales start at key 1
375                                     $key = array_search($value, $scales);
376                                     if ($key === false) {
377                                         echo "<br/>t0 is $t0";
378                                         echo "<br/>grade is $value";
379                                         $status = false;
380                                         import_cleanup($importcode);
381                                         echo $OUTPUT->notification(get_string('badgrade', 'grades'));
382                                         break 3;
383                                     }
384                                     $value = $key;
385                                 }
386                                 $newgrade->finalgrade = $value;
387                             } else {
388                                 if ($value === '' or $value == '-') {
389                                     $value = null; // no grade
391                                 } else if (!is_numeric($value)) {
392                                 // non numeric grade value supplied, possibly mapped wrong column
393                                     echo "<br/>t0 is $t0";
394                                     echo "<br/>grade is $value";
395                                     $status = false;
396                                     import_cleanup($importcode);
397                                     echo $OUTPUT->notification(get_string('badgrade', 'grades'));
398                                     break 3;
399                                 }
400                                 $newgrade->finalgrade = $value;
401                             }
402                             $newgrades[] = $newgrade;
403                         } // otherwise, we ignore this column altogether
404                           // because user has chosen to ignore them (e.g. institution, address etc)
405                     break;
406                 }
407             }
409             // no user mapping supplied at all, or user mapping failed
410             if (empty($studentid) || !is_numeric($studentid)) {
411                 // user not found, abort whole import
412                 $status = false;
413                 import_cleanup($importcode);
414                 echo $OUTPUT->notification('user mapping error, could not find user!');
415                 break;
416             }
418             if ($separatemode and !groups_is_member($currentgroup, $studentid)) {
419                 // not allowed to import into this group, abort
420                 $status = false;
421                 import_cleanup($importcode);
422                 echo $OUTPUT->notification('user not member of current group, can not update!');
423                 break;
424             }
426             // insert results of this students into buffer
427             if ($status and !empty($newgrades)) {
429                 foreach ($newgrades as $newgrade) {
431                     // check if grade_grade is locked and if so, abort
432                     if (!empty($newgrade->itemid) and $grade_grade = new grade_grade(array('itemid'=>$newgrade->itemid, 'userid'=>$studentid))) {
433                         if ($grade_grade->is_locked()) {
434                             // individual grade locked
435                             $status = false;
436                             import_cleanup($importcode);
437                             echo $OUTPUT->notification(get_string('gradelocked', 'grades'));
438                             break 2;
439                         }
440                     }
442                     $newgrade->importcode = $importcode;
443                     $newgrade->userid     = $studentid;
444                     $newgrade->importer   = $USER->id;
445                     $DB->insert_record('grade_import_values', $newgrade);
446                 }
447             }
449             // updating/inserting all comments here
450             if ($status and !empty($newfeedbacks)) {
451                 foreach ($newfeedbacks as $newfeedback) {
452                     $sql = "SELECT *
453                               FROM {grade_import_values}
454                              WHERE importcode=? AND userid=? AND itemid=? AND importer=?";
455                     if ($feedback = $DB->get_record_sql($sql, array($importcode, $studentid, $newfeedback->itemid, $USER->id))) {
456                         $newfeedback->id = $feedback->id;
457                         $DB->update_record('grade_import_values', $newfeedback);
459                     } else {
460                         // the grade item for this is not updated
461                         $newfeedback->importcode = $importcode;
462                         $newfeedback->userid     = $studentid;
463                         $newfeedback->importer   = $USER->id;
464                         $DB->insert_record('grade_import_values', $newfeedback);
465                     }
466                 }
467             }
468         }
470         /// at this stage if things are all ok, we commit the changes from temp table
471         if ($status) {
472             grade_import_commit($course->id, $importcode);
473         }
474         // temporary file can go now
475         fclose($fp);
476         unlink($filename);
477     } else {
478         print_error('cannotreadfil');
479     }
481 } else {
482     groups_print_course_menu($course, 'index.php?id='.$id);
483     echo '<div class="clearer"></div>';
485     // display the standard upload file form
486     $mform->display();
489 echo $OUTPUT->footer();