MDL-57023 tool_usertours: Ensure that the tour key is unique
[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
1093e43b
AN
526 // Notify the cache that a tour has changed.
527 cache::notify_tour_change();
528
001fc061
AN
529 return $this;
530 }
531
532 /**
533 * Remove this step.
534 */
535 public function remove() {
536 global $DB;
537
538 if ($this->id === null) {
539 // Nothing to delete - this tour has not been persisted.
540 return null;
541 }
542
543 // Delete all steps associated with this tour.
544 // Note, although they are currently just DB records, there may be other components in the future.
545 foreach ($this->get_steps() as $step) {
546 $step->remove();
547 }
548
549 // Remove the configuration for the tour.
550 $DB->delete_records('tool_usertours_tours', array('id' => $this->id));
551
552 helper::reset_tour_sortorder();
553
554 return null;
555 }
556
557 /**
558 * Reset the sortorder for all steps in the tour.
559 *
560 * @return $this
561 */
562 public function reset_step_sortorder() {
563 global $DB;
564 $steps = $DB->get_records('tool_usertours_steps', array('tourid' => $this->id), 'sortorder ASC', 'id');
565
566 $index = 0;
567 foreach ($steps as $step) {
568 $DB->set_field('tool_usertours_steps', 'sortorder', $index, array('id' => $step->id));
569 $index++;
570 }
571
1093e43b
AN
572 // Notify of a change to the step configuration.
573 // Note: Do not notify of a tour change here. This is only a step change for a tour.
574 cache::notify_step_change($this->get_id());
575
001fc061
AN
576 return $this;
577 }
578
579 /**
580 * Whether this tour should be displayed to the user.
581 *
582 * @return boolean
583 */
584 public function should_show_for_user() {
585 if (!$this->is_enabled()) {
586 // The tour is disabled - it should not be shown.
587 return false;
588 }
589
590 if ($tourcompletiondate = get_user_preferences(self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id(), null)) {
591 if ($tourresetdate = get_user_preferences(self::TOUR_REQUESTED_BY_USER . $this->get_id(), null)) {
592 if ($tourresetdate >= $tourcompletiondate) {
593 return true;
594 }
595 }
596 $lastmajorupdate = $this->get_config('majorupdatetime', time());
597 if ($tourcompletiondate > $lastmajorupdate) {
598 // The user has completed the tour since the last major update.
599 return false;
600 }
601 }
602
603 return true;
604 }
605
606 /**
607 * Get the key for this tour.
608 * This is used in the session cookie to determine whether the user has seen this tour before.
609 */
610 public function get_tour_key() {
aacdf8e2
AN
611 global $USER;
612
001fc061
AN
613 $tourtime = $this->get_config('majorupdatetime', null);
614
615 if ($tourtime === null) {
616 // This tour has no majorupdate time.
617 // Set one now to prevent repeated displays to the user.
618 $this->set_config('majorupdatetime', time());
619 $this->persist();
620 $tourtime = $this->get_config('majorupdatetime', null);
621 }
622
623 if ($userresetdate = get_user_preferences(self::TOUR_REQUESTED_BY_USER . $this->get_id(), null)) {
624 $tourtime = max($tourtime, $userresetdate);
625 }
626
aacdf8e2 627 return sprintf('tool_usertours_%d_%d_%s', $USER->id, $this->get_id(), $tourtime);
001fc061
AN
628 }
629
630 /**
631 * Reset the requested by user date.
632 *
633 * @return $this
634 */
635 public function request_user_reset() {
636 set_user_preference(self::TOUR_REQUESTED_BY_USER . $this->get_id(), time());
637
638 return $this;
639 }
640
641 /**
642 * Mark this tour as completed for this user.
643 *
644 * @return $this
645 */
646 public function mark_user_completed() {
647 set_user_preference(self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id(), time());
648
649 return $this;
650 }
651
652 /**
653 * Update a tour giving it a new major update time.
654 * This will ensure that it is displayed to all users, even those who have already seen it.
655 *
656 * @return $this
657 */
658 public function mark_major_change() {
659 global $DB;
660
661 // Clear old reset and completion notes.
662 $DB->delete_records('user_preferences', ['name' => self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id()]);
663 $DB->delete_records('user_preferences', ['name' => self::TOUR_REQUESTED_BY_USER . $this->get_id()]);
664 $this->set_config('majorupdatetime', time());
665 $this->persist();
666
667 return $this;
668 }
669
670 /**
671 * Add the step configuration to the form.
672 *
673 * @param MoodleQuickForm $mform The form to add configuration to.
674 * @return $this
675 */
676 public function add_config_to_form(\MoodleQuickForm &$mform) {
677 $options = configuration::get_placement_options();
678 $mform->addElement('select', 'placement', get_string('placement', 'tool_usertours'), $options);
679 $mform->addHelpButton('placement', 'placement', 'tool_usertours');
680
681 $this->add_config_field_to_form($mform, 'orphan');
682 $this->add_config_field_to_form($mform, 'backdrop');
683 $this->add_config_field_to_form($mform, 'reflex');
684
685 return $this;
686 }
687
688 /**
689 * Add the specified step field configuration to the form.
690 *
691 * @param MoodleQuickForm $mform The form to add configuration to.
692 * @param string $key The key to add.
693 * @return $this
694 */
695 protected function add_config_field_to_form(\MoodleQuickForm &$mform, $key) {
696 $options = [
697 true => get_string('yes'),
698 false => get_string('no'),
699 ];
700 $mform->addElement('select', $key, get_string($key, 'tool_usertours'), $options);
701 $mform->setDefault($key, configuration::get_default_value($key));
702 $mform->addHelpButton($key, $key, 'tool_usertours');
703
704 return $this;
705 }
706
707 /**
708 * Prepare the configuration data for the moodle form.
709 *
710 * @return object
711 */
712 public function prepare_data_for_form() {
713 $data = $this->to_record();
714 foreach (configuration::get_defaultable_keys() as $key) {
715 $data->$key = $this->get_config($key, configuration::get_default_value($key));
716 }
717
718 return $data;
719 }
720
721 /**
722 * Get the configured filter values.
723 *
724 * @param string $filter The filter to retrieve values for.
725 * @return array
726 */
727 public function get_filter_values($filter) {
728 if ($allvalues = (array) $this->get_config('filtervalues')) {
729 if (isset($allvalues[$filter])) {
730 return $allvalues[$filter];
731 }
732 }
733
734 return [];
735 }
736
737 /**
738 * Set the values for the specified filter.
739 *
740 * @param string $filter The filter to set.
741 * @param array $values The values to set.
742 * @return $this
743 */
744 public function set_filter_values($filter, array $values = []) {
745 $allvalues = (array) $this->get_config('filtervalues', []);
746 $allvalues[$filter] = $values;
747
748 return $this->set_config('filtervalues', $allvalues);
749 }
750
751 /**
752 * Check whether this tour matches all filters.
753 *
754 * @param context $context The context to check
755 * @return bool
756 */
757 public function matches_all_filters(\context $context) {
758 $filters = helper::get_all_filters();
759
760 // All filters must match.
761 // If any one filter fails to match, we return false.
762 foreach ($filters as $filterclass) {
763 if (!$filterclass::filter_matches($this, $context)) {
764 return false;
765 }
766 }
767
768 return true;
769 }
770}