27438467f301fe46948f6ffe2924d468897b050c
[moodle.git] / admin / tool / uploadcourse / classes / processor.php
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/>.
17 /**
18  * File containing processor class.
19  *
20  * @package    tool_uploadcourse
21  * @copyright  2013 Frédéric Massart
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
26 require_once($CFG->libdir . '/csvlib.class.php');
28 /**
29  * Processor class.
30  *
31  * @package    tool_uploadcourse
32  * @copyright  2013 Frédéric Massart
33  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34  */
35 class tool_uploadcourse_processor {
37     /**
38      * Create courses that do not exist yet.
39      */
40     const MODE_CREATE_NEW = 1;
42     /**
43      * Create all courses, appending a suffix to the shortname if the course exists.
44      */
45     const MODE_CREATE_ALL = 2;
47     /**
48      * Create courses, and update the ones that already exist.
49      */
50     const MODE_CREATE_OR_UPDATE = 3;
52     /**
53      * Only update existing courses.
54      */
55     const MODE_UPDATE_ONLY = 4;
57     /**
58      * During update, do not update anything... O_o Huh?!
59      */
60     const UPDATE_NOTHING = 0;
62     /**
63      * During update, only use data passed from the CSV.
64      */
65     const UPDATE_ALL_WITH_DATA_ONLY = 1;
67     /**
68      * During update, use either data from the CSV, or defaults.
69      */
70     const UPDATE_ALL_WITH_DATA_OR_DEFAUTLS = 2;
72     /**
73      * During update, update missing values from either data from the CSV, or defaults.
74      */
75     const UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS = 3;
77     /** @var int processor mode. */
78     protected $mode;
80     /** @var int upload mode. */
81     protected $updatemode;
83     /** @var bool are renames allowed. */
84     protected $allowrenames = false;
86     /** @var bool are deletes allowed. */
87     protected $allowdeletes = false;
89     /** @var bool are resets allowed. */
90     protected $allowresets = false;
92     /** @var string path to a restore file. */
93     protected $restorefile;
95     /** @var string shortname of the course to be restored. */
96     protected $templatecourse;
98     /** @var string reset courses after processing them. */
99     protected $reset;
101     /** @var string template to generate a course shortname. */
102     protected $shortnametemplate;
104     /** @var csv_import_reader */
105     protected $cir;
107     /** @var array default values. */
108     protected $defaults = array();
110     /** @var array CSV columns. */
111     protected $columns = array();
113     /** @var array of errors where the key is the line number. */
114     protected $errors = array();
116     /** @var int line number. */
117     protected $linenb = 0;
119     /** @var bool whether the process has been started or not. */
120     protected $processstarted = false;
122     /**
123      * Constructor
124      *
125      * @param csv_import_reader $cir import reader object
126      * @param array $options options of the process
127      * @param array $defaults default data value
128      */
129     public function __construct(csv_import_reader $cir, array $options, array $defaults = array()) {
131         if (!isset($options['mode']) || !in_array($options['mode'], array(self::MODE_CREATE_NEW, self::MODE_CREATE_ALL,
132                 self::MODE_CREATE_OR_UPDATE, self::MODE_UPDATE_ONLY))) {
133             throw new coding_exception('Unknown process mode');
134         }
136         // Force int to make sure === comparison work as expected.
137         $this->mode = (int) $options['mode'];
139         $this->updatemode = self::UPDATE_NOTHING;
140         if (isset($options['updatemode'])) {
141             // Force int to make sure === comparison work as expected.
142             $this->updatemode = (int) $options['updatemode'];
143         }
144         if (isset($options['allowrenames'])) {
145             $this->allowrenames = $options['allowrenames'];
146         }
147         if (isset($options['allowdeletes'])) {
148             $this->allowdeletes = $options['allowdeletes'];
149         }
150         if (isset($options['allowresets'])) {
151             $this->allowresets = $options['allowresets'];
152         }
154         if (isset($options['restorefile'])) {
155             $this->restorefile = $options['restorefile'];
156         }
157         if (isset($options['templatecourse'])) {
158             $this->templatecourse = $options['templatecourse'];
159         }
160         if (isset($options['reset'])) {
161             $this->reset = $options['reset'];
162         }
163         if (isset($options['shortnametemplate'])) {
164             $this->shortnametemplate = $options['shortnametemplate'];
165         }
167         $this->cir = $cir;
168         $this->columns = $cir->get_columns();
169         $this->defaults = $defaults;
170         $this->validate();
171         $this->reset();
172     }
174     /**
175      * Execute the process.
176      *
177      * @param object $tracker the output tracker to use.
178      * @return void
179      */
180     public function execute($tracker = null) {
181         if ($this->processstarted) {
182             throw new coding_exception('Process has already been started');
183         }
184         $this->processstarted = true;
186         if (empty($tracker)) {
187             $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::NO_OUTPUT);
188         }
189         $tracker->start();
191         $total = 0;
192         $created = 0;
193         $updated = 0;
194         $deleted = 0;
195         $errors = 0;
197         // We will most certainly need extra time and memory to process big files.
198         core_php_time_limit::raise();
199         raise_memory_limit(MEMORY_EXTRA);
201         // Loop over the CSV lines.
202         while ($line = $this->cir->next()) {
203             $this->linenb++;
204             $total++;
206             $data = $this->parse_line($line);
207             $course = $this->get_course($data);
208             if ($course->prepare()) {
209                 $course->proceed();
210                 $outcome = 1;
212                 $status = $course->get_statuses();
213                 if (array_key_exists('coursecreated', $status)) {
214                     $created++;
215                 } else if (array_key_exists('courseupdated', $status)) {
216                     $updated++;
217                 } else if (array_key_exists('coursedeleted', $status)) {
218                     $deleted++;
219                 }
221                 // Errors can occur even though the course preparation returned true, often because
222                 // some checks could not be done in course::prepare() because it requires the course to exist.
223                 if ($course->has_errors()) {
224                     $status += $course->get_errors();
225                     $errors++;
226                     $outcome = 2;
227                 }
229                 $data = array_merge($data, $course->get_data(), array('id' => $course->get_id()));
230                 $tracker->output($this->linenb, $outcome, $status, $data);
231             } else {
232                 $errors++;
233                 $tracker->output($this->linenb, 0, $course->get_errors(), $data);
234             }
235         }
237         $tracker->finish();
238         $tracker->results($total, $created, $updated, $deleted, $errors);
239     }
241     /**
242      * Return a course import object.
243      *
244      * @param array $data data to import the course with.
245      * @return tool_uploadcourse_course
246      */
247     protected function get_course($data) {
248         $importoptions = array(
249             'candelete' => $this->allowdeletes,
250             'canrename' => $this->allowrenames,
251             'canreset' => $this->allowresets,
252             'reset' => $this->reset,
253             'restoredir' => $this->get_restore_content_dir(),
254             'shortnametemplate' => $this->shortnametemplate
255         );
256         return new tool_uploadcourse_course($this->mode, $this->updatemode, $data, $this->defaults, $importoptions);
257     }
259     /**
260      * Return the errors.
261      *
262      * @return array
263      */
264     public function get_errors() {
265         return $this->errors;
266     }
268     /**
269      * Get the directory of the object to restore.
270      *
271      * @return string subdirectory in $CFG->tempdir/backup/...
272      */
273     protected function get_restore_content_dir() {
274         $backupfile = null;
275         $shortname = null;
277         if (!empty($this->restorefile)) {
278             $backupfile = $this->restorefile;
279         } else if (!empty($this->templatecourse) || is_numeric($this->templatecourse)) {
280             $shortname = $this->templatecourse;
281         }
283         $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname);
284         return $dir;
285     }
287     /**
288      * Log errors on the current line.
289      *
290      * @param array $errors array of errors
291      * @return void
292      */
293     protected function log_error($errors) {
294         if (empty($errors)) {
295             return;
296         }
298         foreach ($errors as $code => $langstring) {
299             if (!isset($this->errors[$this->linenb])) {
300                 $this->errors[$this->linenb] = array();
301             }
302             $this->errors[$this->linenb][$code] = $langstring;
303         }
304     }
306     /**
307      * Parse a line to return an array(column => value)
308      *
309      * @param array $line returned by csv_import_reader
310      * @return array
311      */
312     protected function parse_line($line) {
313         $data = array();
314         foreach ($line as $keynum => $value) {
315             if (!isset($this->columns[$keynum])) {
316                 // This should not happen.
317                 continue;
318             }
320             $key = $this->columns[$keynum];
321             $data[$key] = $value;
322         }
323         return $data;
324     }
326     /**
327      * Return a preview of the import.
328      *
329      * This only returns passed data, along with the errors.
330      *
331      * @param integer $rows number of rows to preview.
332      * @param object $tracker the output tracker to use.
333      * @return array of preview data.
334      */
335     public function preview($rows = 10, $tracker = null) {
336         if ($this->processstarted) {
337             throw new coding_exception('Process has already been started');
338         }
339         $this->processstarted = true;
341         if (empty($tracker)) {
342             $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::NO_OUTPUT);
343         }
344         $tracker->start();
346         // We might need extra time and memory depending on the number of rows to preview.
347         core_php_time_limit::raise();
348         raise_memory_limit(MEMORY_EXTRA);
350         // Loop over the CSV lines.
351         $preview = array();
352         while (($line = $this->cir->next()) && $rows > $this->linenb) {
353             $this->linenb++;
354             $data = $this->parse_line($line);
355             $course = $this->get_course($data);
356             $result = $course->prepare();
357             if (!$result) {
358                 $tracker->output($this->linenb, $result, $course->get_errors(), $data);
359             } else {
360                 $tracker->output($this->linenb, $result, $course->get_statuses(), $data);
361             }
362             $row = $data;
363             $preview[$this->linenb] = $row;
364         }
366         $tracker->finish();
368         return $preview;
369     }
371     /**
372      * Reset the current process.
373      *
374      * @return void.
375      */
376     public function reset() {
377         $this->processstarted = false;
378         $this->linenb = 0;
379         $this->cir->init();
380         $this->errors = array();
381     }
383     /**
384      * Validation.
385      *
386      * @return void
387      */
388     protected function validate() {
389         if (empty($this->columns)) {
390             throw new moodle_exception('cannotreadtmpfile', 'error');
391         } else if (count($this->columns) < 2) {
392             throw new moodle_exception('csvfewcolumns', 'error');
393         }
394     }