MDL-64863 tool_lp: Progress if needed
[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             } else {
278                 // Avoid html output on CLI scripts.
279                 $this->progress = new \core\progress\none();
280             }
281             $this->progress->start_progress('', count($this->flat));
282             // Build a tree from this flat list.
283             raise_memory_limit(MEMORY_EXTRA);
284             $this->add_children($this->framework, '');
285             $this->progress->end_progress();
286         }
287     }
289     /**
290      * Add a competency to the parent with the specified idnumber.
291      *
292      * @param competency $node (pass by reference)
293      * @param string $parentidnumber Add this competency to the parent with this idnumber.
294      */
295     public function add_children(& $node, $parentidnumber) {
296         foreach ($this->flat as $competency) {
297             if ($competency->parentidnumber == $parentidnumber) {
298                 $this->progress->increment_progress();
299                 $node->children[] = $competency;
300                 $this->add_children($competency, $competency->idnumber);
301             }
302         }
303     }
305     /**
306      * Get parse errors.
307      * @return array of errors from parsing the xml.
308      */
309     public function get_error() {
310         return $this->error;
311     }
313     /**
314      * Recursive function to add a competency with all it's children.
315      *
316      * @param stdClass $record Raw data for the new competency
317      * @param competency $parent
318      * @param competency_framework $framework
319      */
320     public function create_competency($record, $parent, $framework) {
321         $competency = new stdClass();
322         $competency->competencyframeworkid = $framework->get('id');
323         $competency->shortname = $record->shortname;
324         if (!empty($record->description)) {
325             $competency->description = $record->description;
326             $competency->descriptionformat = $record->descriptionformat;
327         }
328         if ($record->scalevalues) {
329             $competency->scaleid = $this->get_scale_id($record->scalevalues, $competency->shortname);
330             $competency->scaleconfiguration = $this->get_scale_configuration($competency->scaleid, $record->scaleconfiguration);
331         }
332         if ($parent) {
333             $competency->parentid = $parent->get('id');
334         } else {
335             $competency->parentid = 0;
336         }
337         $competency->idnumber = $record->idnumber;
339         if (!empty($competency->idnumber) && !empty($competency->shortname)) {
340             $comp = api::create_competency($competency);
341             if ($record->exportid) {
342                 $this->mappings[$record->exportid] = $comp;
343             }
344             $record->createdcomp = $comp;
345             foreach ($record->children as $child) {
346                 $this->create_competency($child, $comp, $framework);
347             }
349             return $comp;
350         }
351         return false;
352     }
354     /**
355      * Recreate the scale config to point to a new scaleid.
356      * @param int $scaleid
357      * @param string $config json encoded scale data.
358      */
359     public function get_scale_configuration($scaleid, $config) {
360         $asarray = json_decode($config);
361         $asarray[0]->scaleid = $scaleid;
362         return json_encode($asarray);
363     }
365     /**
366      * Search for a global scale that matches this set of scalevalues.
367      * If one is not found it will be created.
368      * @param array $scalevalues
369      * @param string $competencyname (Used to create a new scale if required)
370      * @return int The id of the scale
371      */
372     public function get_scale_id($scalevalues, $competencyname) {
373         global $CFG, $USER;
375         require_once($CFG->libdir . '/gradelib.php');
377         if (empty($this->scalecache)) {
378             $allscales = grade_scale::fetch_all_global();
379             foreach ($allscales as $scale) {
380                 $scale->load_items();
381                 $this->scalecache[$scale->compact_items()] = $scale;
382             }
383         }
384         $matchingscale = false;
385         if (isset($this->scalecache[$scalevalues])) {
386             $matchingscale = $this->scalecache[$scalevalues];
387         }
388         if (!$matchingscale) {
389             // Create it.
390             $newscale = new grade_scale();
391             $newscale->name = get_string('competencyscale', 'tool_lpimportcsv', $competencyname);
392             $newscale->courseid = 0;
393             $newscale->userid = $USER->id;
394             $newscale->scale = $scalevalues;
395             $newscale->description = get_string('competencyscaledescription', 'tool_lpimportcsv');
396             $newscale->insert();
397             $this->scalecache[$scalevalues] = $newscale;
398             return $newscale->id;
399         }
400         return $matchingscale->id;
401     }
403     /**
404      * Walk through the idnumbers in the relatedidnumbers col and set the relations.
405      * @param stdClass $record
406      */
407     protected function set_related($record) {
408         $comp = $record->createdcomp;
409         if ($record->relatedidnumbers) {
410             $allidnumbers = explode(',', $record->relatedidnumbers);
411             foreach ($allidnumbers as $rawidnumber) {
412                 $idnumber = str_replace('%2C', ',', $rawidnumber);
414                 if (isset($this->flat[$idnumber])) {
415                     $relatedcomp = $this->flat[$idnumber]->createdcomp;
416                     api::add_related_competency($comp->get('id'), $relatedcomp->get('id'));
417                 }
418             }
419         }
420         foreach ($record->children as $child) {
421             $this->set_related($child);
422         }
423     }
425     /**
426      * Create any completion rule attached to this competency.
427      * @param stdClass $record
428      */
429     protected function set_rules($record) {
430         $comp = $record->createdcomp;
431         if ($record->ruletype) {
432             $class = $record->ruletype;
433             if (class_exists($class)) {
434                 $oldruleconfig = $record->ruleconfig;
435                 if ($oldruleconfig == "null") {
436                     $oldruleconfig = null;
437                 }
438                 $newruleconfig = $class::migrate_config($oldruleconfig, $this->mappings);
439                 $comp->set('ruleconfig', $newruleconfig);
440                 $comp->set('ruletype', $class);
441                 $comp->set('ruleoutcome', $record->ruleoutcome);
442                 $comp->update();
443             }
444         }
445         foreach ($record->children as $child) {
446             $this->set_rules($child);
447         }
448     }
450     /**
451      * Do the job.
452      * @return competency_framework
453      */
454     public function import() {
455         $record = clone $this->framework;
456         unset($record->children);
458         $record->scaleid = $this->get_scale_id($record->scalevalues, $record->shortname);
459         $record->scaleconfiguration = $this->get_scale_configuration($record->scaleid, $record->scaleconfiguration);
460         unset($record->scalevalues);
461         $record->contextid = context_system::instance()->id;
463         $framework = api::create_framework($record);
464         if ($this->useprogressbar === true) {
465             $this->progress = new \core\progress\display_if_slow(get_string('importingfile', 'tool_lpimportcsv'));
466         } else {
467             $this->progress = new \core\progress\none();
468         }
470         $this->progress->start_progress('', (count($this->framework->children) * 2));
471         raise_memory_limit(MEMORY_EXTRA);
472         // Now all the children.
473         foreach ($this->framework->children as $comp) {
474             $this->progress->increment_progress();
475             $this->create_competency($comp, null, $framework);
476         }
478         // Now create the rules.
479         foreach ($this->framework->children as $record) {
480             $this->progress->increment_progress();
481             $this->set_rules($record);
482             $this->set_related($record);
483         }
484         $this->progress->end_progress();
486         $this->importer->cleanup();
487         return $framework;
488     }