MDL-52777 tool_usertours: Stop testing pending upgrades
[moodle.git] / admin / tool / usertours / classes / manager.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  * Tour manager.
19  *
20  * @package    tool_usertours
21  * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace tool_usertours;
27 defined('MOODLE_INTERNAL') || die();
29 use tool_usertours\local\forms;
30 use tool_usertours\local\table;
32 /**
33  * Tour manager.
34  *
35  * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
36  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 class manager {
40     /**
41      * @var ACTION_LISTTOURS      The action to get the list of tours.
42      */
43     const ACTION_LISTTOURS = 'listtours';
45     /**
46      * @var ACTION_NEWTOUR        The action to create a new tour.
47      */
48     const ACTION_NEWTOUR = 'newtour';
50     /**
51      * @var ACTION_EDITTOUR       The action to edit the tour.
52      */
53     const ACTION_EDITTOUR = 'edittour';
55     /**
56      * @var ACTION_MOVETOUR The action to move a tour up or down.
57      */
58     const ACTION_MOVETOUR = 'movetour';
60     /**
61      * @var ACTION_EXPORTTOUR     The action to export the tour.
62      */
63     const ACTION_EXPORTTOUR = 'exporttour';
65     /**
66      * @var ACTION_IMPORTTOUR     The action to import the tour.
67      */
68     const ACTION_IMPORTTOUR = 'importtour';
70     /**
71      * @var ACTION_DELETETOUR     The action to delete the tour.
72      */
73     const ACTION_DELETETOUR = 'deletetour';
75     /**
76      * @var ACTION_VIEWTOUR       The action to view the tour.
77      */
78     const ACTION_VIEWTOUR = 'viewtour';
80     /**
81      * @var ACTION_NEWSTEP The action to create a new step.
82      */
83     const ACTION_NEWSTEP = 'newstep';
85     /**
86      * @var ACTION_EDITSTEP The action to edit step configuration.
87      */
88     const ACTION_EDITSTEP = 'editstep';
90     /**
91      * @var ACTION_MOVESTEP The action to move a step up or down.
92      */
93     const ACTION_MOVESTEP = 'movestep';
95     /**
96      * @var ACTION_DELETESTEP The action to delete a step.
97      */
98     const ACTION_DELETESTEP = 'deletestep';
100     /**
101      * @var ACTION_VIEWSTEP The action to view a step.
102      */
103     const ACTION_VIEWSTEP = 'viewstep';
105     /**
106      * @var ACTION_HIDETOUR The action to hide a tour.
107      */
108     const ACTION_HIDETOUR = 'hidetour';
110     /**
111      * @var ACTION_SHOWTOUR The action to show a tour.
112      */
113     const ACTION_SHOWTOUR = 'showtour';
115     /**
116      * @var ACTION_RESETFORALL
117      */
118     const ACTION_RESETFORALL = 'resetforall';
120     /**
121      * This is the entry point for this controller class.
122      *
123      * @param   string  $action     The action to perform.
124      */
125     public function execute($action) {
126         admin_externalpage_setup('tool_usertours/tours');
127         // Add the main content.
128         switch($action) {
129             case self::ACTION_NEWTOUR:
130             case self::ACTION_EDITTOUR:
131                 $this->edit_tour(optional_param('id', null, PARAM_INT));
132                 break;
134             case self::ACTION_MOVETOUR:
135                 $this->move_tour(required_param('id', PARAM_INT));
136                 break;
138             case self::ACTION_EXPORTTOUR:
139                 $this->export_tour(required_param('id', PARAM_INT));
140                 break;
142             case self::ACTION_IMPORTTOUR:
143                 $this->import_tour();
144                 break;
146             case self::ACTION_VIEWTOUR:
147                 $this->view_tour(required_param('id', PARAM_INT));
148                 break;
150             case self::ACTION_HIDETOUR:
151                 $this->hide_tour(required_param('id', PARAM_INT));
152                 break;
154             case self::ACTION_SHOWTOUR:
155                 $this->show_tour(required_param('id', PARAM_INT));
156                 break;
158             case self::ACTION_DELETETOUR:
159                 $this->delete_tour(required_param('id', PARAM_INT));
160                 break;
162             case self::ACTION_RESETFORALL:
163                 $this->reset_tour_for_all(required_param('id', PARAM_INT));
164                 break;
166             case self::ACTION_NEWSTEP:
167             case self::ACTION_EDITSTEP:
168                 $this->edit_step(optional_param('id', null, PARAM_INT));
169                 break;
171             case self::ACTION_MOVESTEP:
172                 $this->move_step(required_param('id', PARAM_INT));
173                 break;
175             case self::ACTION_DELETESTEP:
176                 $this->delete_step(required_param('id', PARAM_INT));
177                 break;
179             case self::ACTION_LISTTOURS:
180             default:
181                 $this->print_tour_list();
182                 break;
183         }
184     }
186     /**
187      * Print out the page header.
188      *
189      * @param   string  $title     The title to display.
190      */
191     protected function header($title = null) {
192         global $OUTPUT;
194         // Print the page heading.
195         echo $OUTPUT->header();
197         if ($title === null) {
198             $title = get_string('tours', 'tool_usertours');
199         }
201         echo $OUTPUT->heading($title);
202     }
204     /**
205      * Print out the page footer.
206      *
207      * @return void
208      */
209     protected function footer() {
210         global $OUTPUT;
212         echo $OUTPUT->footer();
213     }
215     /**
216      * Print the the list of tours.
217      */
218     protected function print_tour_list() {
219         global $PAGE, $OUTPUT;
221         $this->header();
222         echo \html_writer::span(get_string('tourlist_explanation', 'tool_usertours'));
223         $table = new table\tour_list();
224         $tours = helper::get_tours();
225         foreach ($tours as $tour) {
226             $table->add_data_keyed($table->format_row($tour));
227         }
229         $table->finish_output();
230         $actions = [
231             (object) [
232                 'link'  => helper::get_edit_tour_link(),
233                 'linkproperties' => [],
234                 'img'   => 'b/tour-new',
235                 'title' => get_string('newtour', 'tool_usertours'),
236             ],
237             (object) [
238                 'link'  => helper::get_import_tour_link(),
239                 'linkproperties' => [],
240                 'img'   => 'b/tour-import',
241                 'title' => get_string('importtour', 'tool_usertours'),
242             ],
243             (object) [
244                 'link'  => new \moodle_url('https://moodle.net/mod/data/view.php', [
245                         'id' => 17,
246                     ]),
247                 'linkproperties' => [
248                         'target' => '_blank',
249                     ],
250                 'img'   => 'b/tour-shared',
251                 'title' => get_string('sharedtourslink', 'tool_usertours'),
252             ],
253         ];
255         echo \html_writer::start_tag('div', [
256                 'class' => 'tour-actions',
257             ]);
259         echo \html_writer::start_tag('ul');
260         foreach ($actions as $config) {
261             $action = \html_writer::start_tag('li');
262             $linkproperties = $config->linkproperties;
263             $linkproperties['href'] = $config->link;
264             $action .= \html_writer::start_tag('a', $linkproperties);
265             $action .= \html_writer::img(
266                 $OUTPUT->pix_url($config->img, 'tool_usertours'),
267                 $config->title);
268             $action .= \html_writer::div($config->title);
269             $action .= \html_writer::end_tag('a');
270             $action .= \html_writer::end_tag('li');
271             echo $action;
272         }
273         echo \html_writer::end_tag('ul');
274         echo \html_writer::end_tag('div');
276         // JS for Tour management.
277         $PAGE->requires->js_call_amd('tool_usertours/managetours', 'setup');
278         $this->footer();
279     }
281     /**
282      * Return the edit tour link.
283      *
284      * @param   int         $id     The ID of the tour
285      * @return string
286      */
287     protected function get_edit_tour_link($id = null) {
288         $addlink = helper::get_edit_tour_link($id);
289         return \html_writer::link($addlink, get_string('newtour', 'tool_usertours'));
290     }
292     /**
293      * Print the edit tour link.
294      *
295      * @param   int         $id     The ID of the tour
296      */
297     protected function print_edit_tour_link($id = null) {
298         echo $this->get_edit_tour_link($id);
299     }
301     /**
302      * Get the import tour link.
303      *
304      * @return string
305      */
306     protected function get_import_tour_link() {
307         $importlink = helper::get_import_tour_link();
308         return \html_writer::link($importlink, get_string('importtour', 'tool_usertours'));
309     }
311     /**
312      * Print the edit tour page.
313      *
314      * @param   int         $id     The ID of the tour
315      */
316     protected function edit_tour($id = null) {
317         global $PAGE;
318         if ($id) {
319             $tour = tour::instance($id);
320             $PAGE->navbar->add($tour->get_name(), $tour->get_edit_link());
322         } else {
323             $tour = new tour();
324             $PAGE->navbar->add(get_string('newtour', 'tool_usertours'), $tour->get_edit_link());
325         }
327         $form = new forms\edittour($tour);
329         if ($form->is_cancelled()) {
330             redirect(helper::get_list_tour_link());
331         } else if ($data = $form->get_data()) {
332             // Creating a new tour.
333             $tour->set_name($data->name);
334             $tour->set_description($data->description);
335             $tour->set_pathmatch($data->pathmatch);
336             $tour->set_enabled(!empty($data->enabled));
338             foreach (configuration::get_defaultable_keys() as $key) {
339                 $tour->set_config($key, $data->$key);
340             }
342             // Save filter values.
343             foreach (helper::get_all_filters() as $filterclass) {
344                 $filterclass::save_filter_values_from_form($tour, $data);
345             }
347             $tour->persist();
349             redirect(helper::get_list_tour_link());
350         } else {
351             if (empty($tour)) {
352                 $this->header('newtour');
353             } else {
354                 $this->header($tour->get_name());
355                 $data = $tour->prepare_data_for_form();
357                 // Prepare filter values for the form.
358                 foreach (helper::get_all_filters() as $filterclass) {
359                     $filterclass::prepare_filter_values_for_form($tour, $data);
360                 }
361                 $form->set_data($data);
362             }
364             $form->display();
365             $this->footer();
366         }
367     }
369     /**
370      * Print the export tour page.
371      *
372      * @param   int         $id     The ID of the tour
373      */
374     protected function export_tour($id) {
375         $tour = tour::instance($id);
377         // Grab the full data record.
378         $export = $tour->to_record();
380         // Remove the id.
381         unset($export->id);
383         // Set the version.
384         $export->version = get_config('tool_usertours', 'version');
386         // Step export.
387         $export->steps = [];
388         foreach ($tour->get_steps() as $step) {
389             $record = $step->to_record();
390             unset($record->id);
391             unset($record->tourid);
393             $export->steps[] = $record;
394         }
396         $exportstring = json_encode($export);
398         $filename = 'tour_export_' . $tour->get_id() . '_' . time() . '.json';
400         // Force download.
401         header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
402         header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0');
403         header('Expires: ' . gmdate('D, d M Y H:i:s', 0) . 'GMT');
404         header('Pragma: no-cache');
405         header('Accept-Ranges: none');
406         header('Content-disposition: attachment; filename=' . $filename);
407         header('Content-length: ' . strlen($exportstring));
408         header('Content-type: text/calendar; charset=utf-8');
410         echo $exportstring;
411         die;
412     }
414     /**
415      * Handle tour import.
416      */
417     protected function import_tour() {
418         global $PAGE;
419         $PAGE->navbar->add(get_string('importtour', 'tool_usertours'), helper::get_import_tour_link());
421         $form = new forms\importtour();
423         if ($form->is_cancelled()) {
424             redirect(helper::get_list_tour_link());
425         } else if ($form->get_data()) {
426             // Importing a tour.
427             $tourconfigraw = $form->get_file_content('tourconfig');
428             $tour = self::import_tour_from_json($tourconfigraw);
430             redirect($tour->get_view_link());
431         } else {
432             $this->header();
433             $form->display();
434             $this->footer();
435         }
436     }
438     /**
439      * Print the view tour page.
440      *
441      * @param   int         $tourid     The ID of the tour to display.
442      */
443     protected function view_tour($tourid) {
444         global $PAGE;
445         $tour = helper::get_tour($tourid);
447         $PAGE->navbar->add($tour->get_name(), $tour->get_view_link());
449         $this->header($tour->get_name());
450         echo \html_writer::span(get_string('viewtour_info', 'tool_usertours', [
451                 'tourname'  => $tour->get_name(),
452                 'path'      => $tour->get_pathmatch(),
453             ]));
454         echo \html_writer::div(get_string('viewtour_edit', 'tool_usertours', [
455                 'editlink'  => $tour->get_edit_link()->out(),
456                 'resetlink' => $tour->get_reset_link()->out(),
457             ]));
459         $table = new table\step_list($tourid);
460         foreach ($tour->get_steps() as $step) {
461             $table->add_data_keyed($table->format_row($step));
462         }
464         $table->finish_output();
465         $this->print_edit_step_link($tourid);
467         // JS for Step management.
468         $PAGE->requires->js_call_amd('tool_usertours/managesteps', 'setup');
470         $this->footer();
471     }
473     /**
474      * Show the tour.
475      *
476      * @param   int         $tourid     The ID of the tour to display.
477      */
478     protected function show_tour($tourid) {
479         $this->show_hide_tour($tourid, 1);
480     }
482     /**
483      * Hide the tour.
484      *
485      * @param   int         $tourid     The ID of the tour to display.
486      */
487     protected function hide_tour($tourid) {
488         $this->show_hide_tour($tourid, 0);
489     }
491     /**
492      * Show or Hide the tour.
493      *
494      * @param   int         $tourid     The ID of the tour to display.
495      * @param   int         $visibility The intended visibility.
496      */
497     protected function show_hide_tour($tourid, $visibility) {
498         global $DB;
500         require_sesskey();
502         $tour = $DB->get_record('tool_usertours_tours', array('id' => $tourid));
503         $tour->enabled = $visibility;
504         $DB->update_record('tool_usertours_tours', $tour);
506         redirect(helper::get_list_tour_link());
507     }
509     /**
510      * Delete the tour.
511      *
512      * @param   int         $tourid     The ID of the tour to remove.
513      */
514     protected function delete_tour($tourid) {
515         require_sesskey();
517         $tour = tour::instance($tourid);
518         $tour->remove();
520         redirect(helper::get_list_tour_link());
521     }
523     /**
524      * Reset the tour state for all users.
525      *
526      * @param   int         $tourid     The ID of the tour to remove.
527      */
528     protected function reset_tour_for_all($tourid) {
529         require_sesskey();
531         $tour = tour::instance($tourid);
532         $tour->mark_major_change();
534         redirect(helper::get_view_tour_link($tourid), get_string('tour_resetforall', 'tool_usertours'));
535     }
537     /**
538      * Get the first tour matching the current page URL.
539      *
540      * @param   bool        $reset      Forcibly update the current tour
541      * @return  tour
542      */
543     public static function get_current_tour($reset = false) {
544         global $PAGE;
546         static $tour = false;
548         if ($tour === false || $reset) {
549             $tour = self::get_matching_tours($PAGE->url);
550         }
552         return $tour;
553     }
555     /**
556      * Get the first tour matching the specified URL.
557      *
558      * @param   moodle_url  $pageurl        The URL to match.
559      * @return  tour
560      */
561     public static function get_matching_tours(\moodle_url $pageurl) {
562         global $DB, $PAGE;
564         $sql = <<<EOF
565             SELECT * FROM {tool_usertours_tours}
566              WHERE enabled = 1
567                AND ? LIKE pathmatch
568           ORDER BY sortorder ASC
569 EOF;
571         $tours = $DB->get_records_sql($sql, array(
572             $pageurl->out_as_local_url(),
573         ));
575         foreach ($tours as $record) {
576             $tour = tour::load_from_record($record);
577             if ($tour->is_enabled() && $tour->matches_all_filters($PAGE->context) && $tour->count_steps() > 0) {
578                 return $tour;
579             }
580         }
582         return null;
583     }
585     /**
586      * Import the provided tour JSON.
587      *
588      * @param   string      $json           The tour configuration.
589      * @return  tour
590      */
591     public static function import_tour_from_json($json) {
592         $tourconfig = json_decode($json);
594         // We do not use this yet - we may do in the future.
595         unset($tourconfig->version);
597         $steps = $tourconfig->steps;
598         unset($tourconfig->steps);
600         $tourconfig->id = null;
601         $tourconfig->sortorder = null;
602         $tour = tour::load_from_record($tourconfig, true);
603         $tour->persist(true);
605         // Ensure that steps are orderered by their sortorder.
606         \core_collator::asort_objects_by_property($steps, 'sortorder', \core_collator::SORT_NUMERIC);
608         foreach ($steps as $stepconfig) {
609             $stepconfig->id = null;
610             $stepconfig->tourid = $tour->get_id();
611             $step = step::load_from_record($stepconfig, true);
612             $step->persist(true);
613         }
615         return $tour;
616     }
618     /**
619      * Helper to fetch the renderer.
620      *
621      * @return  renderer
622      */
623     protected function get_renderer() {
624         global $PAGE;
625         return $PAGE->get_renderer('tool_usertours');
626     }
628     /**
629      * Print the edit step link.
630      *
631      * @param   int     $tourid     The ID of the tour.
632      * @param   int     $stepid     The ID of the step.
633      * @return  string
634      */
635     protected function print_edit_step_link($tourid, $stepid = null) {
636         $addlink = helper::get_edit_step_link($tourid, $stepid);
637         $attributes = [];
638         if (empty($stepid)) {
639             $attributes['class'] = 'createstep';
640         }
641         echo \html_writer::link($addlink, get_string('newstep', 'tool_usertours'), $attributes);
642     }
644     /**
645      * Display the edit step form for the specified step.
646      *
647      * @param   int     $id     The step to edit.
648      */
649     protected function edit_step($id) {
650         global $PAGE;
652         if (isset($id)) {
653             $step = step::instance($id);
654         } else {
655             $step = new step();
656             $step->set_tourid(required_param('tourid', PARAM_INT));
657         }
659         $tour = $step->get_tour();
660         $PAGE->navbar->add($tour->get_name(), $tour->get_view_link());
661         if (isset($id)) {
662             $PAGE->navbar->add($step->get_title(), $step->get_edit_link());
663         } else {
664             $PAGE->navbar->add(get_string('newstep', 'tool_usertours'), $step->get_edit_link());
665         }
667         $form = new forms\editstep($step->get_edit_link(), $step);
668         if ($form->is_cancelled()) {
669             redirect($step->get_tour()->get_view_link());
670         } else if ($data = $form->get_data()) {
671             $step->handle_form_submission($form, $data);
672             $step->get_tour()->reset_step_sortorder();
673             redirect($step->get_tour()->get_view_link());
674         } else {
675             if (empty($id)) {
676                 $this->header(get_string('newstep', 'tool_usertours'));
677             } else {
678                 $this->header(get_string('editstep', 'tool_usertours', $step->get_title()));
679             }
680             $form->set_data($step->prepare_data_for_form());
682             $form->display();
683             $this->footer();
684         }
685     }
687     /**
688      * Move a tour up or down.
689      *
690      * @param   int     $id     The tour to move.
691      */
692     protected function move_tour($id) {
693         require_sesskey();
695         $direction = required_param('direction', PARAM_INT);
697         $tour = tour::instance($id);
698         $currentsortorder   = $tour->get_sortorder();
699         $targetsortorder    = $currentsortorder + $direction;
701         $swapwith = helper::get_tour_from_sortorder($targetsortorder);
703         // Set the sort order to something out of the way.
704         $tour->set_sortorder(-1);
705         $tour->persist();
707         // Swap the two sort orders.
708         $swapwith->set_sortorder($currentsortorder);
709         $swapwith->persist();
711         $tour->set_sortorder($targetsortorder);
712         $tour->persist();
714         redirect(helper::get_list_tour_link());
715     }
717     /**
718      * Move a step up or down.
719      *
720      * @param   int     $id     The step to move.
721      */
722     protected function move_step($id) {
723         require_sesskey();
725         $direction = required_param('direction', PARAM_INT);
727         $step = step::instance($id);
728         $currentsortorder   = $step->get_sortorder();
729         $targetsortorder    = $currentsortorder + $direction;
731         $tour = $step->get_tour();
732         $swapwith = helper::get_step_from_sortorder($tour->get_id(), $targetsortorder);
734         // Set the sort order to something out of the way.
735         $step->set_sortorder(-1);
736         $step->persist();
738         // Swap the two sort orders.
739         $swapwith->set_sortorder($currentsortorder);
740         $swapwith->persist();
742         $step->set_sortorder($targetsortorder);
743         $step->persist();
745         // Reset the sort order.
746         $tour->reset_step_sortorder();
747         redirect($tour->get_view_link());
748     }
750     /**
751      * Delete the step.
752      *
753      * @param   int         $stepid     The ID of the step to remove.
754      */
755     protected function delete_step($stepid) {
756         require_sesskey();
758         $step = step::instance($stepid);
759         $tour = $step->get_tour();
761         $step->remove();
762         redirect($tour->get_view_link());
763     }