on-demand release 4.0dev+
[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();
211                 $status = $course->get_statuses();
212                 if (array_key_exists('coursecreated', $status)) {
213                     $created++;
214                 } else if (array_key_exists('courseupdated', $status)) {
215                     $updated++;
216                 } else if (array_key_exists('coursedeleted', $status)) {
217                     $deleted++;
218                 }
220                 $data = array_merge($data, $course->get_data(), array('id' => $course->get_id()));
221                 $tracker->output($this->linenb, true, $status, $data);
222             } else {
223                 $errors++;
224                 $tracker->output($this->linenb, false, $course->get_errors(), $data);
225             }
226         }
228         $tracker->finish();
229         $tracker->results($total, $created, $updated, $deleted, $errors);
230     }
232     /**
233      * Return a course import object.
234      *
235      * @param array $data data to import the course with.
236      * @return tool_uploadcourse_course
237      */
238     protected function get_course($data) {
239         $importoptions = array(
240             'candelete' => $this->allowdeletes,
241             'canrename' => $this->allowrenames,
242             'canreset' => $this->allowresets,
243             'reset' => $this->reset,
244             'restoredir' => $this->get_restore_content_dir(),
245             'shortnametemplate' => $this->shortnametemplate
246         );
247         return new tool_uploadcourse_course($this->mode, $this->updatemode, $data, $this->defaults, $importoptions);
248     }
250     /**
251      * Return the errors.
252      *
253      * @return array
254      */
255     public function get_errors() {
256         return $this->errors;
257     }
259     /**
260      * Get the directory of the object to restore.
261      *
262      * @return string subdirectory in $CFG->backuptempdir/...
263      */
264     protected function get_restore_content_dir() {
265         $backupfile = null;
266         $shortname = null;
268         if (!empty($this->restorefile)) {
269             $backupfile = $this->restorefile;
270         } else if (!empty($this->templatecourse) || is_numeric($this->templatecourse)) {
271             $shortname = $this->templatecourse;
272         }
274         $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname);
275         return $dir;
276     }
278     /**
279      * Log errors on the current line.
280      *
281      * @param array $errors array of errors
282      * @return void
283      */
284     protected function log_error($errors) {
285         if (empty($errors)) {
286             return;
287         }
289         foreach ($errors as $code => $langstring) {
290             if (!isset($this->errors[$this->linenb])) {
291                 $this->errors[$this->linenb] = array();
292             }
293             $this->errors[$this->linenb][$code] = $langstring;
294         }
295     }
297     /**
298      * Parse a line to return an array(column => value)
299      *
300      * @param array $line returned by csv_import_reader
301      * @return array
302      */
303     protected function parse_line($line) {
304         $data = array();
305         foreach ($line as $keynum => $value) {
306             if (!isset($this->columns[$keynum])) {
307                 // This should not happen.
308                 continue;
309             }
311             $key = $this->columns[$keynum];
312             $data[$key] = $value;
313         }
314         return $data;
315     }
317     /**
318      * Return a preview of the import.
319      *
320      * This only returns passed data, along with the errors.
321      *
322      * @param integer $rows number of rows to preview.
323      * @param object $tracker the output tracker to use.
324      * @return array of preview data.
325      */
326     public function preview($rows = 10, $tracker = null) {
327         if ($this->processstarted) {
328             throw new coding_exception('Process has already been started');
329         }
330         $this->processstarted = true;
332         if (empty($tracker)) {
333             $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::NO_OUTPUT);
334         }
335         $tracker->start();
337         // We might need extra time and memory depending on the number of rows to preview.
338         core_php_time_limit::raise();
339         raise_memory_limit(MEMORY_EXTRA);
341         // Loop over the CSV lines.
342         $preview = array();
343         while (($line = $this->cir->next()) && $rows > $this->linenb) {
344             $this->linenb++;
345             $data = $this->parse_line($line);
346             $course = $this->get_course($data);
347             $result = $course->prepare();
348             if (!$result) {
349                 $tracker->output($this->linenb, $result, $course->get_errors(), $data);
350             } else {
351                 $tracker->output($this->linenb, $result, $course->get_statuses(), $data);
352             }
353             $row = $data;
354             $preview[$this->linenb] = $row;
355         }
357         $tracker->finish();
359         return $preview;
360     }
362     /**
363      * Reset the current process.
364      *
365      * @return void.
366      */
367     public function reset() {
368         $this->processstarted = false;
369         $this->linenb = 0;
370         $this->cir->init();
371         $this->errors = array();
372     }
374     /**
375      * Validation.
376      *
377      * @return void
378      */
379     protected function validate() {
380         if (empty($this->columns)) {
381             throw new moodle_exception('cannotreadtmpfile', 'error');
382         } else if (count($this->columns) < 2) {
383             throw new moodle_exception('csvfewcolumns', 'error');
384         }
385     }