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