MDL-52777 tool_usertours: Add the new User Tours plugin
[moodle.git] / admin / tool / usertours / classes / tour.php
CommitLineData
001fc061
AN
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/>.
16
17/**
18 * Tour class.
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 */
24
25namespace tool_usertours;
26
27defined('MOODLE_INTERNAL') || die();
28
29/**
30 * Tour class.
31 *
32 * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 */
35class tour {
36
37 /**
38 * The tour is currently disabled
39 *
40 * @var DISABLED
41 */
42 const DISABLED = 0;
43
44 /**
45 * The tour is currently disabled
46 *
47 * @var DISABLED
48 */
49 const ENABLED = 1;
50
51 /**
52 * The user preference value to indicate the time of completion of the tour for a user.
53 *
54 * @var TOUR_LAST_COMPLETED_BY_USER
55 */
56 const TOUR_LAST_COMPLETED_BY_USER = 'tool_usertours_tour_completion_time_';
57
58 /**
59 * The user preference value to indicate the time that a user last requested to see the tour.
60 *
61 * @var TOUR_REQUESTED_BY_USER
62 */
63 const TOUR_REQUESTED_BY_USER = 'tool_usertours_tour_reset_time_';
64
65 /**
66 * @var $id The tour ID.
67 */
68 protected $id;
69
70 /**
71 * @var $name The tour name.
72 */
73 protected $name;
74
75 /**
76 * @var $description The tour description.
77 */
78 protected $description;
79
80 /**
81 * @var $pathmatch The tour pathmatch.
82 */
83 protected $pathmatch;
84
85 /**
86 * @var $enabled The tour enabled state.
87 */
88 protected $enabled;
89
90 /**
91 * @var $sortorder The sort order.
92 */
93 protected $sortorder;
94
95 /**
96 * @var $dirty Whether the current view of the tour has been modified.
97 */
98 protected $dirty = false;
99
100 /**
101 * @var $config The configuration object for the tour.
102 */
103 protected $config;
104
105 /**
106 * @var $filtervalues The filter configuration object for the tour.
107 */
108 protected $filtervalues;
109
110 /**
111 * @var $steps The steps in this tour.
112 */
113 protected $steps = [];
114
115 /**
116 * Create an instance of the specified tour.
117 *
118 * @param int $id The ID of the tour to load.
119 * @return tour
120 */
121 public static function instance($id) {
122 $tour = new self();
123 return $tour->fetch($id);
124 }
125
126 /**
127 * Create an instance of tour from its provided DB record.
128 *
129 * @param stdClass $record The record of the tour to load.
130 * @param boolean $clean Clean the values.
131 * @return tour
132 */
133 public static function load_from_record($record, $clean = false) {
134 $tour = new self();
135 return $tour->reload_from_record($record, $clean);
136 }
137
138 /**
139 * Fetch the specified tour into the current object.
140 *
141 * @param int $id The ID of the tour to fetch.
142 * @return tour
143 */
144 protected function fetch($id) {
145 global $DB;
146
147 return $this->reload_from_record(
148 $DB->get_record('tool_usertours_tours', array('id' => $id), '*', MUST_EXIST)
149 );
150 }
151
152 /**
153 * Reload the current tour from database.
154 *
155 * @return tour
156 */
157 protected function reload() {
158 return $this->fetch($this->id);
159 }
160
161 /**
162 * Reload the tour into the current object.
163 *
164 * @param stdClass $record The record to reload.
165 * @param boolean $clean Clean the values.
166 * @return tour
167 */
168 protected function reload_from_record($record, $clean = false) {
169 $this->id = $record->id;
170 if (!property_exists($record, 'description')) {
171 if (property_exists($record, 'comment')) {
172 $record->description = $record->comment;
173 unset($record->comment);
174 }
175 }
176 if ($clean) {
177 $this->name = clean_param($record->name, PARAM_TEXT);
178 $this->description = clean_text($record->description);
179 } else {
180 $this->name = $record->name;
181 $this->description = $record->description;
182 }
183 $this->pathmatch = $record->pathmatch;
184 $this->enabled = $record->enabled;
185 if (isset($record->sortorder)) {
186 $this->sortorder = $record->sortorder;
187 }
188 $this->config = json_decode($record->configdata);
189 $this->dirty = false;
190 $this->steps = [];
191
192 return $this;
193 }
194
195 /**
196 * Fetch all steps in the tour.
197 *
198 * @return stdClass[]
199 */
200 public function get_steps() {
201 if (empty($this->steps)) {
202 $this->steps = helper::get_steps($this->id);
203 }
204
205 return $this->steps;
206 }
207
208 /**
209 * Count the number of steps in the tour.
210 *
211 * @return int
212 */
213 public function count_steps() {
214 return count($this->get_steps());
215 }
216
217 /**
218 * The ID of the tour.
219 *
220 * @return int
221 */
222 public function get_id() {
223 return $this->id;
224 }
225
226 /**
227 * The name of the tour.
228 *
229 * @return string
230 */
231 public function get_name() {
232 return $this->name;
233 }
234
235 /**
236 * Set the name of the tour to the specified value.
237 *
238 * @param string $value The new name.
239 * @return $this
240 */
241 public function set_name($value) {
242 $this->name = clean_param($value, PARAM_TEXT);
243 $this->dirty = true;
244
245 return $this;
246 }
247
248 /**
249 * The description associated with the tour.
250 *
251 * @return string
252 */
253 public function get_description() {
254 return $this->description;
255 }
256
257 /**
258 * Set the description of the tour to the specified value.
259 *
260 * @param string $value The new description.
261 * @return $this
262 */
263 public function set_description($value) {
264 $this->description = clean_text($value);
265 $this->dirty = true;
266
267 return $this;
268 }
269
270 /**
271 * The path match for the tour.
272 *
273 * @return string
274 */
275 public function get_pathmatch() {
276 return $this->pathmatch;
277 }
278
279 /**
280 * Set the patchmatch of the tour to the specified value.
281 *
282 * @param string $value The new patchmatch.
283 * @return $this
284 */
285 public function set_pathmatch($value) {
286 $this->pathmatch = $value;
287 $this->dirty = true;
288
289 return $this;
290 }
291
292 /**
293 * The enabled state of the tour.
294 *
295 * @return int
296 */
297 public function get_enabled() {
298 return $this->enabled;
299 }
300
301 /**
302 * Whether the tour is currently enabled.
303 *
304 * @return boolean
305 */
306 public function is_enabled() {
307 return ($this->enabled == self::ENABLED);
308 }
309
310 /**
311 * Set the enabled state of the tour to the specified value.
312 *
313 * @param boolean $value The new state.
314 * @return $this
315 */
316 public function set_enabled($value) {
317 $this->enabled = $value;
318 $this->dirty = true;
319
320 return $this;
321 }
322
323 /**
324 * The link to view this tour.
325 *
326 * @return moodle_url
327 */
328 public function get_view_link() {
329 return helper::get_view_tour_link($this->id);
330 }
331
332 /**
333 * The link to edit this tour.
334 *
335 * @return moodle_url
336 */
337 public function get_edit_link() {
338 return helper::get_edit_tour_link($this->id);
339 }
340
341 /**
342 * The link to reset the state of this tour for all users.
343 *
344 * @return moodle_url
345 */
346 public function get_reset_link() {
347 return helper::get_reset_tour_for_all_link($this->id);
348 }
349
350 /**
351 * The link to export this tour.
352 *
353 * @return moodle_url
354 */
355 public function get_export_link() {
356 return helper::get_export_tour_link($this->id);
357 }
358
359 /**
360 * The link to remove this tour.
361 *
362 * @return moodle_url
363 */
364 public function get_delete_link() {
365 return helper::get_delete_tour_link($this->id);
366 }
367
368 /**
369 * Prepare this tour for saving to the database.
370 *
371 * @return object
372 */
373 public function to_record() {
374 return (object) array(
375 'id' => $this->id,
376 'name' => $this->name,
377 'description' => $this->description,
378 'pathmatch' => $this->pathmatch,
379 'enabled' => $this->enabled,
380 'sortorder' => $this->sortorder,
381 'configdata' => json_encode($this->config),
382 );
383 }
384
385 /**
386 * Get the current sortorder for this tour.
387 *
388 * @return int
389 */
390 public function get_sortorder() {
391 return (int) $this->sortorder;
392 }
393
394 /**
395 * Whether this tour is the first tour.
396 *
397 * @return boolean
398 */
399 public function is_first_tour() {
400 return ($this->get_sortorder() === 0);
401 }
402
403 /**
404 * Whether this tour is the last tour.
405 *
406 * @param int $tourcount The pre-fetched count of tours
407 * @return boolean
408 */
409 public function is_last_tour($tourcount = null) {
410 if ($tourcount === null) {
411 $tourcount = helper::count_tours();
412 }
413 return ($this->get_sortorder() === ($tourcount - 1));
414 }
415
416 /**
417 * Set the sortorder for this tour.
418 *
419 * @param int $value The new sortorder to use.
420 * @return $this
421 */
422 public function set_sortorder($value) {
423 $this->sortorder = $value;
424 $this->dirty = true;
425
426 return $this;
427 }
428
429 /**
430 * Calculate the next sort-order value.
431 *
432 * @return int
433 */
434 protected function calculate_sortorder() {
435 $this->sortorder = helper::count_tours();
436
437 return $this;
438 }
439
440 /**
441 * Get the link to move this tour up in the sortorder.
442 *
443 * @return moodle_url
444 */
445 public function get_moveup_link() {
446 return helper::get_move_tour_link($this->get_id(), helper::MOVE_UP);
447 }
448
449 /**
450 * Get the link to move this tour down in the sortorder.
451 *
452 * @return moodle_url
453 */
454 public function get_movedown_link() {
455 return helper::get_move_tour_link($this->get_id(), helper::MOVE_DOWN);
456 }
457
458 /**
459 * Get the value of the specified configuration item.
460 *
461 * @param string $key The configuration key to set.
462 * @param mixed $default The default value to use if a value was not found.
463 * @return mixed
464 */
465 public function get_config($key = null, $default = null) {
466 if ($this->config === null) {
467 $this->config = (object) array();
468 }
469 if ($key === null) {
470 return $this->config;
471 }
472
473 if (property_exists($this->config, $key)) {
474 return $this->config->$key;
475 }
476
477 if ($default !== null) {
478 return $default;
479 }
480
481 return configuration::get_default_value($key);
482 }
483
484 /**
485 * Set the configuration item as specified.
486 *
487 * @param string $key The configuration key to set.
488 * @param mixed $value The new value for the configuration item.
489 * @return $this
490 */
491 public function set_config($key, $value) {
492 if ($this->config === null) {
493 $this->config = (object) array();
494 }
495 $this->config->$key = $value;
496 $this->dirty = true;
497
498 return $this;
499 }
500
501 /**
502 * Save the tour and it's configuration to the database.
503 *
504 * @param boolean $force Whether to force writing to the database.
505 * @return $this
506 */
507 public function persist($force = false) {
508 global $DB;
509
510 if (!$this->dirty && !$force) {
511 return $this;
512 }
513
514 if ($this->id) {
515 $record = $this->to_record();
516 $DB->update_record('tool_usertours_tours', $record);
517 } else {
518 $this->calculate_sortorder();
519 $record = $this->to_record();
520 unset($record->id);
521 $this->id = $DB->insert_record('tool_usertours_tours', $record);
522 }
523
524 $this->reload();
525
526 return $this;
527 }
528
529 /**
530 * Remove this step.
531 */
532 public function remove() {
533 global $DB;
534
535 if ($this->id === null) {
536 // Nothing to delete - this tour has not been persisted.
537 return null;
538 }
539
540 // Delete all steps associated with this tour.
541 // Note, although they are currently just DB records, there may be other components in the future.
542 foreach ($this->get_steps() as $step) {
543 $step->remove();
544 }
545
546 // Remove the configuration for the tour.
547 $DB->delete_records('tool_usertours_tours', array('id' => $this->id));
548
549 helper::reset_tour_sortorder();
550
551 return null;
552 }
553
554 /**
555 * Reset the sortorder for all steps in the tour.
556 *
557 * @return $this
558 */
559 public function reset_step_sortorder() {
560 global $DB;
561 $steps = $DB->get_records('tool_usertours_steps', array('tourid' => $this->id), 'sortorder ASC', 'id');
562
563 $index = 0;
564 foreach ($steps as $step) {
565 $DB->set_field('tool_usertours_steps', 'sortorder', $index, array('id' => $step->id));
566 $index++;
567 }
568
569 return $this;
570 }
571
572 /**
573 * Whether this tour should be displayed to the user.
574 *
575 * @return boolean
576 */
577 public function should_show_for_user() {
578 if (!$this->is_enabled()) {
579 // The tour is disabled - it should not be shown.
580 return false;
581 }
582
583 if ($tourcompletiondate = get_user_preferences(self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id(), null)) {
584 if ($tourresetdate = get_user_preferences(self::TOUR_REQUESTED_BY_USER . $this->get_id(), null)) {
585 if ($tourresetdate >= $tourcompletiondate) {
586 return true;
587 }
588 }
589 $lastmajorupdate = $this->get_config('majorupdatetime', time());
590 if ($tourcompletiondate > $lastmajorupdate) {
591 // The user has completed the tour since the last major update.
592 return false;
593 }
594 }
595
596 return true;
597 }
598
599 /**
600 * Get the key for this tour.
601 * This is used in the session cookie to determine whether the user has seen this tour before.
602 */
603 public function get_tour_key() {
604 $tourtime = $this->get_config('majorupdatetime', null);
605
606 if ($tourtime === null) {
607 // This tour has no majorupdate time.
608 // Set one now to prevent repeated displays to the user.
609 $this->set_config('majorupdatetime', time());
610 $this->persist();
611 $tourtime = $this->get_config('majorupdatetime', null);
612 }
613
614 if ($userresetdate = get_user_preferences(self::TOUR_REQUESTED_BY_USER . $this->get_id(), null)) {
615 $tourtime = max($tourtime, $userresetdate);
616 }
617
618 return sprintf('tool_usertours_%d_%s', $this->get_id(), $tourtime);
619 }
620
621 /**
622 * Reset the requested by user date.
623 *
624 * @return $this
625 */
626 public function request_user_reset() {
627 set_user_preference(self::TOUR_REQUESTED_BY_USER . $this->get_id(), time());
628
629 return $this;
630 }
631
632 /**
633 * Mark this tour as completed for this user.
634 *
635 * @return $this
636 */
637 public function mark_user_completed() {
638 set_user_preference(self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id(), time());
639
640 return $this;
641 }
642
643 /**
644 * Update a tour giving it a new major update time.
645 * This will ensure that it is displayed to all users, even those who have already seen it.
646 *
647 * @return $this
648 */
649 public function mark_major_change() {
650 global $DB;
651
652 // Clear old reset and completion notes.
653 $DB->delete_records('user_preferences', ['name' => self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id()]);
654 $DB->delete_records('user_preferences', ['name' => self::TOUR_REQUESTED_BY_USER . $this->get_id()]);
655 $this->set_config('majorupdatetime', time());
656 $this->persist();
657
658 return $this;
659 }
660
661 /**
662 * Add the step configuration to the form.
663 *
664 * @param MoodleQuickForm $mform The form to add configuration to.
665 * @return $this
666 */
667 public function add_config_to_form(\MoodleQuickForm &$mform) {
668 $options = configuration::get_placement_options();
669 $mform->addElement('select', 'placement', get_string('placement', 'tool_usertours'), $options);
670 $mform->addHelpButton('placement', 'placement', 'tool_usertours');
671
672 $this->add_config_field_to_form($mform, 'orphan');
673 $this->add_config_field_to_form($mform, 'backdrop');
674 $this->add_config_field_to_form($mform, 'reflex');
675
676 return $this;
677 }
678
679 /**
680 * Add the specified step field configuration to the form.
681 *
682 * @param MoodleQuickForm $mform The form to add configuration to.
683 * @param string $key The key to add.
684 * @return $this
685 */
686 protected function add_config_field_to_form(\MoodleQuickForm &$mform, $key) {
687 $options = [
688 true => get_string('yes'),
689 false => get_string('no'),
690 ];
691 $mform->addElement('select', $key, get_string($key, 'tool_usertours'), $options);
692 $mform->setDefault($key, configuration::get_default_value($key));
693 $mform->addHelpButton($key, $key, 'tool_usertours');
694
695 return $this;
696 }
697
698 /**
699 * Prepare the configuration data for the moodle form.
700 *
701 * @return object
702 */
703 public function prepare_data_for_form() {
704 $data = $this->to_record();
705 foreach (configuration::get_defaultable_keys() as $key) {
706 $data->$key = $this->get_config($key, configuration::get_default_value($key));
707 }
708
709 return $data;
710 }
711
712 /**
713 * Get the configured filter values.
714 *
715 * @param string $filter The filter to retrieve values for.
716 * @return array
717 */
718 public function get_filter_values($filter) {
719 if ($allvalues = (array) $this->get_config('filtervalues')) {
720 if (isset($allvalues[$filter])) {
721 return $allvalues[$filter];
722 }
723 }
724
725 return [];
726 }
727
728 /**
729 * Set the values for the specified filter.
730 *
731 * @param string $filter The filter to set.
732 * @param array $values The values to set.
733 * @return $this
734 */
735 public function set_filter_values($filter, array $values = []) {
736 $allvalues = (array) $this->get_config('filtervalues', []);
737 $allvalues[$filter] = $values;
738
739 return $this->set_config('filtervalues', $allvalues);
740 }
741
742 /**
743 * Check whether this tour matches all filters.
744 *
745 * @param context $context The context to check
746 * @return bool
747 */
748 public function matches_all_filters(\context $context) {
749 $filters = helper::get_all_filters();
750
751 // All filters must match.
752 // If any one filter fails to match, we return false.
753 foreach ($filters as $filterclass) {
754 if (!$filterclass::filter_matches($this, $context)) {
755 return false;
756 }
757 }
758
759 return true;
760 }
761}