Merge branch 'MDL-57338-master' of git://github.com/danpoltawski/moodle
[moodle.git] / admin / tool / lpimportcsv / classes / framework_importer.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  * This file contains the class to import a competency framework.
19  *
20  * @package   tool_lpimportcsv
21  * @copyright 2015 Damyon Wiese
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace tool_lpimportcsv;
27 defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.');
29 use core_competency\api;
30 use grade_scale;
31 use stdClass;
32 use context_system;
33 use csv_import_reader;
35 /**
36  * This file contains the class to import a competency framework.
37  *
38  * @package   tool_lpimportcsv
39  * @copyright 2015 Damyon Wiese
40  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41  */
42 class framework_importer {
44     /** @var string $error The errors message from reading the xml */
45     protected $error = '';
47     /** @var array $flat The flat competencies tree */
48     protected $flat = array();
49     /** @var array $framework The framework info */
50     protected $framework = array();
51     protected $mappings = array();
52     protected $importid = 0;
53     protected $importer = null;
54     protected $foundheaders = array();
55     protected $scalecache = array();
56     /** @var bool $useprogressbar Control whether importing should use progress bars or not. */
57     protected $useprogressbar = false;
58     /** @var \core\progress\display_if_slow|null $progress The progress bar instance. */
59     protected $progress = null;
61     /**
62      * Store an error message for display later
63      * @param string $msg
64      */
65     public function fail($msg) {
66         $this->error = $msg;
67         return false;
68     }
70     /**
71      * Get the CSV import id
72      * @return string The import id.
73      */
74     public function get_importid() {
75         return $this->importid;
76     }
78     /**
79      * Get the list of headers required for import.
80      * @return array The headers (lang strings)
81      */
82     public static function list_required_headers() {
83         return array(
84             get_string('parentidnumber', 'tool_lpimportcsv'),
85             get_string('idnumber', 'tool_lpimportcsv'),
86             get_string('shortname', 'tool_lpimportcsv'),
87             get_string('description', 'tool_lpimportcsv'),
88             get_string('descriptionformat', 'tool_lpimportcsv'),
89             get_string('scalevalues', 'tool_lpimportcsv'),
90             get_string('scaleconfiguration', 'tool_lpimportcsv'),
91             get_string('ruletype', 'tool_lpimportcsv'),
92             get_string('ruleoutcome', 'tool_lpimportcsv'),
93             get_string('ruleconfig', 'tool_lpimportcsv'),
94             get_string('relatedidnumbers', 'tool_lpimportcsv'),
95             get_string('exportid', 'tool_lpimportcsv'),
96             get_string('isframework', 'tool_lpimportcsv'),
97             get_string('taxonomy', 'tool_lpimportcsv'),
98         );
99     }
101     /**
102      * Get the list of headers found in the import.
103      * @return array The found headers (names from import)
104      */
105     public function list_found_headers() {
106         return $this->foundheaders;
107     }
109     /**
110      * Read the data from the mapping form.
111      * @param array The mapping data.
112      */
113     protected function read_mapping_data($data) {
114         if ($data) {
115             return array(
116                 'parentidnumber' => $data->header0,
117                 'idnumber' => $data->header1,
118                 'shortname' => $data->header2,
119                 'description' => $data->header3,
120                 'descriptionformat' => $data->header4,
121                 'scalevalues' => $data->header5,
122                 'scaleconfiguration' => $data->header6,
123                 'ruletype' => $data->header7,
124                 'ruleoutcome' => $data->header8,
125                 'ruleconfig' => $data->header9,
126                 'relatedidnumbers' => $data->header10,
127                 'exportid' => $data->header11,
128                 'isframework' => $data->header12,
129                 'taxonomies' => $data->header13
130             );
131         } else {
132             return array(
133                 'parentidnumber' => 0,
134                 'idnumber' => 1,
135                 'shortname' => 2,
136                 'description' => 3,
137                 'descriptionformat' => 4,
138                 'scalevalues' => 5,
139                 'scaleconfiguration' => 6,
140                 'ruletype' => 7,
141                 'ruleoutcome' => 8,
142                 'ruleconfig' => 9,
143                 'relatedidnumbers' => 10,
144                 'exportid' => 11,
145                 'isframework' => 12,
146                 'taxonomies' => 13
147             );
148         }
149     }
151     /**
152      * Get the a column from the imported data.
153      * @param array The imported raw row
154      * @param index The column index we want
155      * @return string The column data.
156      */
157     protected function get_column_data($row, $index) {
158         if ($index < 0) {
159             return '';
160         }
161         return isset($row[$index]) ? $row[$index] : '';
162     }
164     /**
165      * Constructor - parses the raw text for sanity.
166      * @param string $text The raw csv text.
167      * @param string $encoding The encoding of the csv file.
168      * @param string delimiter The specified delimiter for the file.
169      * @param string importid The id of the csv import.
170      * @param array mappingdata The mapping data from the import form.
171      * @param bool $useprogressbar Whether progress bar should be displayed, to avoid html output on CLI.
172      */
173     public function __construct($text = null, $encoding = null, $delimiter = null, $importid = 0, $mappingdata = null,
174             $useprogressbar = false) {
176         global $CFG;
178         // The format of our records is:
179         // Parent ID number, ID number, Shortname, Description, Description format, Scale values, Scale configuration,
180         // Rule type, Rule outcome, Rule config, Is framework, Taxonomy.
182         // The idnumber is concatenated with the category names.
183         require_once($CFG->libdir . '/csvlib.class.php');
185         $type = 'competency_framework';
187         if (!$importid) {
188             if ($text === null) {
189                 return;
190             }
191             $this->importid = csv_import_reader::get_new_iid($type);
193             $this->importer = new csv_import_reader($this->importid, $type);
195             if (!$this->importer->load_csv_content($text, $encoding, $delimiter)) {
196                 $this->fail(get_string('invalidimportfile', 'tool_lpimportcsv'));
197                 $this->importer->cleanup();
198                 return;
199             }
201         } else {
202             $this->importid = $importid;
204             $this->importer = new csv_import_reader($this->importid, $type);
205         }
207         if (!$this->importer->init()) {
208             $this->fail(get_string('invalidimportfile', 'tool_lpimportcsv'));
209             $this->importer->cleanup();
210             return;
211         }
213         $this->foundheaders = $this->importer->get_columns();
214         $this->useprogressbar = $useprogressbar;
215         $domainid = 1;
217         $flat = array();
218         $framework = null;
220         while ($row = $this->importer->next()) {
221             $mapping = $this->read_mapping_data($mappingdata);
223             $parentidnumber = $this->get_column_data($row, $mapping['parentidnumber']);
224             $idnumber = $this->get_column_data($row, $mapping['idnumber']);
225             $shortname = $this->get_column_data($row, $mapping['shortname']);
226             $description = $this->get_column_data($row, $mapping['description']);
227             $descriptionformat = $this->get_column_data($row, $mapping['descriptionformat']);
228             $scalevalues = $this->get_column_data($row, $mapping['scalevalues']);
229             $scaleconfiguration = $this->get_column_data($row, $mapping['scaleconfiguration']);
230             $ruletype = $this->get_column_data($row, $mapping['ruletype']);
231             $ruleoutcome = $this->get_column_data($row, $mapping['ruleoutcome']);
232             $ruleconfig = $this->get_column_data($row, $mapping['ruleconfig']);
233             $relatedidnumbers = $this->get_column_data($row, $mapping['relatedidnumbers']);
234             $exportid = $this->get_column_data($row, $mapping['exportid']);
235             $isframework = $this->get_column_data($row, $mapping['isframework']);
236             $taxonomies = $this->get_column_data($row, $mapping['taxonomies']);
238             if ($isframework) {
239                 $framework = new stdClass();
240                 $framework->idnumber = shorten_text(clean_param($idnumber, PARAM_TEXT), 100);
241                 $framework->shortname = shorten_text(clean_param($shortname, PARAM_TEXT), 100);
242                 $framework->description = clean_param($description, PARAM_RAW);
243                 $framework->descriptionformat = clean_param($descriptionformat, PARAM_INT);
244                 $framework->scalevalues = $scalevalues;
245                 $framework->scaleconfiguration = $scaleconfiguration;
246                 $framework->taxonomies = $taxonomies;
247                 $framework->children = array();
248             } else {
249                 $competency = new stdClass();
250                 $competency->parentidnumber = clean_param($parentidnumber, PARAM_TEXT);
251                 $competency->idnumber = shorten_text(clean_param($idnumber, PARAM_TEXT), 100);
252                 $competency->shortname = shorten_text(clean_param($shortname, PARAM_TEXT), 100);
253                 $competency->description = clean_param($description, PARAM_RAW);
254                 $competency->descriptionformat = clean_param($descriptionformat, PARAM_INT);
255                 $competency->ruletype = $ruletype;
256                 $competency->ruleoutcome = clean_param($ruleoutcome, PARAM_INT);
257                 $competency->ruleconfig = $ruleconfig;
258                 $competency->relatedidnumbers = $relatedidnumbers;
259                 $competency->exportid = $exportid;
260                 $competency->scalevalues = $scalevalues;
261                 $competency->scaleconfiguration = $scaleconfiguration;
262                 $competency->children = array();
263                 $flat[$idnumber] = $competency;
264             }
265         }
266         $this->flat = $flat;
267         $this->framework = $framework;
269         $this->importer->close();
270         if ($this->framework == null) {
271             $this->fail(get_string('invalidimportfile', 'tool_lpimportcsv'));
272             return;
273         } else {
274             // We are calling from browser, display progress bar.
275             if ($this->useprogressbar === true) {
276                 $this->progress = new \core\progress\display_if_slow(get_string('processingfile', 'tool_lpimportcsv'));
277                 $this->progress->start_html();
278             } else {
279                 // Avoid html output on CLI scripts.
280                 $this->progress = new \core\progress\none();
281             }
282             $this->progress->start_progress('', count($this->flat));
283             // Build a tree from this flat list.
284             raise_memory_limit(MEMORY_EXTRA);
285             $this->add_children($this->framework, '');
286             $this->progress->end_progress();
287         }
288     }
290     /**
291      * Add a competency to the parent with the specified idnumber.
292      *
293      * @param competency $node (pass by reference)
294      * @param string $parentidnumber Add this competency to the parent with this idnumber.
295      */
296     public function add_children(& $node, $parentidnumber) {
297         foreach ($this->flat as $competency) {
298             if ($competency->parentidnumber == $parentidnumber) {
299                 $this->progress->increment_progress();
300                 $node->children[] = $competency;
301                 $this->add_children($competency, $competency->idnumber);
302             }
303         }
304     }
306     /**
307      * Get parse errors.
308      * @return array of errors from parsing the xml.
309      */
310     public function get_error() {
311         return $this->error;
312     }
314     /**
315      * Recursive function to add a competency with all it's children.
316      *
317      * @param stdClass $record Raw data for the new competency
318      * @param competency $parent
319      * @param competency_framework $framework
320      */
321     public function create_competency($record, $parent, $framework) {
322         $competency = new stdClass();
323         $competency->competencyframeworkid = $framework->get_id();
324         $competency->shortname = $record->shortname;
325         if (!empty($record->description)) {
326             $competency->description = $record->description;
327             $competency->descriptionformat = $record->descriptionformat;
328         }
329         if ($record->scalevalues) {
330             $competency->scaleid = $this->get_scale_id($record->scalevalues, $competency->shortname);
331             $competency->scaleconfiguration = $this->get_scale_configuration($competency->scaleid, $record->scaleconfiguration);
332         }
333         if ($parent) {
334             $competency->parentid = $parent->get_id();
335         } else {
336             $competency->parentid = 0;
337         }
338         $competency->idnumber = $record->idnumber;
340         if (!empty($competency->idnumber) && !empty($competency->shortname)) {
341             $comp = api::create_competency($competency);
342             if ($record->exportid) {
343                 $this->mappings[$record->exportid] = $comp;
344             }
345             $record->createdcomp = $comp;
346             foreach ($record->children as $child) {
347                 $this->create_competency($child, $comp, $framework);
348             }
350             return $comp;
351         }
352         return false;
353     }
355     /**
356      * Recreate the scale config to point to a new scaleid.
357      * @param int $scaleid
358      * @param string $config json encoded scale data.
359      */
360     public function get_scale_configuration($scaleid, $config) {
361         $asarray = json_decode($config);
362         $asarray[0]->scaleid = $scaleid;
363         return json_encode($asarray);
364     }
366     /**
367      * Search for a global scale that matches this set of scalevalues.
368      * If one is not found it will be created.
369      * @param array $scalevalues
370      * @param string $competencyname (Used to create a new scale if required)
371      * @return int The id of the scale
372      */
373     public function get_scale_id($scalevalues, $competencyname) {
374         global $CFG, $USER;
376         require_once($CFG->libdir . '/gradelib.php');
378         if (empty($this->scalecache)) {
379             $allscales = grade_scale::fetch_all_global();
380             foreach ($allscales as $scale) {
381                 $scale->load_items();
382                 $this->scalecache[$scale->compact_items()] = $scale;
383             }
384         }
385         $matchingscale = false;
386         if (isset($this->scalecache[$scalevalues])) {
387             $matchingscale = $this->scalecache[$scalevalues];
388         }
389         if (!$matchingscale) {
390             // Create it.
391             $newscale = new grade_scale();
392             $newscale->name = get_string('competencyscale', 'tool_lpimportcsv', $competencyname);
393             $newscale->courseid = 0;
394             $newscale->userid = $USER->id;
395             $newscale->scale = $scalevalues;
396             $newscale->description = get_string('competencyscaledescription', 'tool_lpimportcsv');
397             $newscale->insert();
398             $this->scalecache[$scalevalues] = $newscale;
399             return $newscale->id;
400         }
401         return $matchingscale->id;
402     }
404     /**
405      * Walk through the idnumbers in the relatedidnumbers col and set the relations.
406      * @param stdClass $record
407      */
408     protected function set_related($record) {
409         $comp = $record->createdcomp;
410         if ($record->relatedidnumbers) {
411             $allidnumbers = explode(',', $record->relatedidnumbers);
412             foreach ($allidnumbers as $rawidnumber) {
413                 $idnumber = str_replace('%2C', ',', $rawidnumber);
415                 if (isset($this->flat[$idnumber])) {
416                     $relatedcomp = $this->flat[$idnumber]->createdcomp;
417                     api::add_related_competency($comp->get_id(), $relatedcomp->get_id());
418                 }
419             }
420         }
421         foreach ($record->children as $child) {
422             $this->set_related($child);
423         }
424     }
426     /**
427      * Create any completion rule attached to this competency.
428      * @param stdClass $record
429      */
430     protected function set_rules($record) {
431         $comp = $record->createdcomp;
432         if ($record->ruletype) {
433             $class = $record->ruletype;
434             if (class_exists($class)) {
435                 $oldruleconfig = $record->ruleconfig;
436                 if ($oldruleconfig == "null") {
437                     $oldruleconfig = null;
438                 }
439                 $newruleconfig = $class::migrate_config($oldruleconfig, $this->mappings);
440                 $comp->set_ruleconfig($newruleconfig);
441                 $comp->set_ruletype($class);
442                 $comp->set_ruleoutcome($record->ruleoutcome);
443                 $comp->update();
444             }
445         }
446         foreach ($record->children as $child) {
447             $this->set_rules($child);
448         }
449     }
451     /**
452      * Do the job.
453      * @return competency_framework
454      */
455     public function import() {
456         $record = clone $this->framework;
457         unset($record->children);
459         $record->scaleid = $this->get_scale_id($record->scalevalues, $record->shortname);
460         $record->scaleconfiguration = $this->get_scale_configuration($record->scaleid, $record->scaleconfiguration);
461         unset($record->scalevalues);
462         $record->contextid = context_system::instance()->id;
464         $framework = api::create_framework($record);
465         if ($this->useprogressbar === true) {
466             $this->progress = new \core\progress\display_if_slow(get_string('importingfile', 'tool_lpimportcsv'));
467             $this->progress->start_html();
468         } else {
469             $this->progress = new \core\progress\none();
470         }
472         $this->progress->start_progress('', (count($this->framework->children) * 2));
473         raise_memory_limit(MEMORY_EXTRA);
474         // Now all the children.
475         foreach ($this->framework->children as $comp) {
476             $this->progress->increment_progress();
477             $this->create_competency($comp, null, $framework);
478         }
480         // Now create the rules.
481         foreach ($this->framework->children as $record) {
482             $this->progress->increment_progress();
483             $this->set_rules($record);
484             $this->set_related($record);
485         }
486         $this->progress->end_progress();
488         $this->importer->cleanup();
489         return $framework;
490     }