Merge branch 'MDL-63959-master' of git://github.com/tobiasreischmann/moodle
[moodle.git] / mod / feedback / classes / completion.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  * Contains class mod_feedback_completion
19  *
20  * @package   mod_feedback
21  * @copyright 2016 Marina Glancy
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 /**
28  * Collects information and methods about feedback completion (either complete.php or show_entries.php)
29  *
30  * @package   mod_feedback
31  * @copyright 2016 Marina Glancy
32  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33  */
34 class mod_feedback_completion extends mod_feedback_structure {
35     /** @var stdClass */
36     protected $completed;
37     /** @var stdClass */
38     protected $completedtmp = null;
39     /** @var stdClass[] */
40     protected $valuestmp = null;
41     /** @var stdClass[] */
42     protected $values = null;
43     /** @var bool */
44     protected $iscompleted = false;
45     /** @var mod_feedback_complete_form the form used for completing the feedback */
46     protected $form = null;
47     /** @var bool true when the feedback has been completed during the request */
48     protected $justcompleted = false;
49     /** @var int the next page the user should jump after processing the form */
50     protected $jumpto = null;
53     /**
54      * Constructor
55      *
56      * @param stdClass $feedback feedback object
57      * @param cm_info $cm course module object corresponding to the $feedback
58      *     (at least one of $feedback or $cm is required)
59      * @param int $courseid current course (for site feedbacks only)
60      * @param bool $iscompleted has feedback been already completed? If yes either completedid or userid must be specified.
61      * @param int $completedid id in the table feedback_completed, may be omitted if userid is specified
62      *     but it is highly recommended because the same user may have multiple responses to the same feedback
63      *     for different courses
64      * @param int $nonanonymouseuserid - Return only anonymous results or specified user's results.
65      *     If null only anonymous replies will be returned and the $completedid is mandatory.
66      *     If specified only non-anonymous replies of $nonanonymouseuserid will be returned.
67      * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default).
68      */
69     public function __construct($feedback, $cm, $courseid, $iscompleted = false, $completedid = null,
70                                 $nonanonymouseuserid = null, $userid = 0) {
71         global $DB;
73         parent::__construct($feedback, $cm, $courseid, 0, $userid);
74         // Make sure courseid is always set for site feedback.
75         if ($this->feedback->course == SITEID && !$this->courseid) {
76             $this->courseid = SITEID;
77         }
78         if ($iscompleted) {
79             // Retrieve information about the completion.
80             $this->iscompleted = true;
81             $params = array('feedback' => $this->feedback->id);
82             if (!$nonanonymouseuserid && !$completedid) {
83                 throw new coding_exception('Either $completedid or $nonanonymouseuserid must be specified for completed feedbacks');
84             }
85             if ($completedid) {
86                 $params['id'] = $completedid;
87             }
88             if ($nonanonymouseuserid) {
89                 // We must respect the anonymousity of the reply that the user saw when they were completing the feedback,
90                 // not the current state that may have been changed later by the teacher.
91                 $params['anonymous_response'] = FEEDBACK_ANONYMOUS_NO;
92                 $params['userid'] = $nonanonymouseuserid;
93             }
94             $this->completed = $DB->get_record('feedback_completed', $params, '*', MUST_EXIST);
95             $this->courseid = $this->completed->courseid;
96         }
97     }
99     /**
100      * Returns a record from 'feedback_completed' table
101      * @return stdClass
102      */
103     public function get_completed() {
104         return $this->completed;
105     }
107     /**
108      * Check if the feedback was just completed.
109      *
110      * @return bool true if the feedback was just completed.
111      * @since  Moodle 3.3
112      */
113     public function just_completed() {
114         return $this->justcompleted;
115     }
117     /**
118      * Return the jumpto property.
119      *
120      * @return int the next page to jump.
121      * @since  Moodle 3.3
122      */
123     public function get_jumpto() {
124         return $this->jumpto;
125     }
127     /**
128      * Returns the temporary completion record for the current user or guest session
129      *
130      * @return stdClass|false record from feedback_completedtmp or false if not found
131      */
132     public function get_current_completed_tmp() {
133         global $DB, $USER;
134         if ($this->completedtmp === null) {
135             $params = array('feedback' => $this->get_feedback()->id);
136             if ($courseid = $this->get_courseid()) {
137                 $params['courseid'] = $courseid;
138             }
139             if ((isloggedin() || $USER->id != $this->userid) && !isguestuser($this->userid)) {
140                 $params['userid'] = $this->userid;
141             } else {
142                 $params['guestid'] = sesskey();
143             }
144             $this->completedtmp = $DB->get_record('feedback_completedtmp', $params);
145         }
146         return $this->completedtmp;
147     }
149     /**
150      * Can the current user see the item, if dependency is met?
151      *
152      * @param stdClass $item
153      * @return bool whether user can see item or not,
154      *     true if there is no dependency or dependency is met,
155      *     false if dependent question is visible or broken
156      *        and further it is either not answered or the dependency is not met,
157      *     null if dependency is broken.
158      */
159     protected function can_see_item($item) {
160         if (empty($item->dependitem)) {
161             return true;
162         }
163         if ($this->dependency_has_error($item)) {
164             return null;
165         }
166         $allitems = $this->get_items();
167         $ditem = $allitems[$item->dependitem];
168         $itemobj = feedback_get_item_class($ditem->typ);
169         if ($this->iscompleted) {
170             $value = $this->get_values($ditem);
171         } else {
172             $value = $this->get_values_tmp($ditem);
173         }
174         if ($value === null) {
175             // Cyclic dependencies are no problem here, since they will throw an dependency error above.
176             if ($this->can_see_item($ditem) === false) {
177                 return false;
178             }
179             return null;
180         }
181         return $itemobj->compare_value($ditem, $value, $item->dependvalue) ? true : false;
182     }
184     /**
185      * Dependency condition has an error
186      * @param stdClass $item
187      * @return bool
188      */
189     protected function dependency_has_error($item) {
190         if (empty($item->dependitem)) {
191             // No dependency - no error.
192             return false;
193         }
194         $allitems = $this->get_items();
195         if (!array_key_exists($item->dependitem, $allitems)) {
196             // Looks like dependent item has been removed.
197             return true;
198         }
199         $itemids = array_keys($allitems);
200         $index1 = array_search($item->dependitem, $itemids);
201         $index2 = array_search($item->id, $itemids);
202         if ($index1 >= $index2) {
203             // Dependent item is after the current item in the feedback.
204             return true;
205         }
206         for ($i = $index1 + 1; $i < $index2; $i++) {
207             if ($allitems[$itemids[$i]]->typ === 'pagebreak') {
208                 return false;
209             }
210         }
211         // There are no page breaks between dependent items.
212         return true;
213     }
215     /**
216      * Returns a value stored for this item in the feedback (temporary or not, depending on the mode)
217      * @param stdClass $item
218      * @return string
219      */
220     public function get_item_value($item) {
221         if ($this->iscompleted) {
222             return $this->get_values($item);
223         } else {
224             return $this->get_values_tmp($item);
225         }
226     }
228     /**
229      * Retrieves responses from an unfinished attempt.
230      *
231      * @return array the responses (from the feedback_valuetmp table)
232      * @since  Moodle 3.3
233      */
234     public function get_unfinished_responses() {
235         global $DB;
236         $responses = array();
238         $completedtmp = $this->get_current_completed_tmp();
239         if ($completedtmp) {
240             $responses = $DB->get_records('feedback_valuetmp', ['completed' => $completedtmp->id]);
241         }
242         return $responses;
243     }
245     /**
246      * Returns all temporary values for this feedback or just a value for an item
247      * @param stdClass $item
248      * @return array
249      */
250     protected function get_values_tmp($item = null) {
251         global $DB;
252         if ($this->valuestmp === null) {
253             $this->valuestmp = array();
254             $responses = $this->get_unfinished_responses();
255             foreach ($responses as $r) {
256                 $this->valuestmp[$r->item] = $r->value;
257             }
258         }
259         if ($item) {
260             return array_key_exists($item->id, $this->valuestmp) ? $this->valuestmp[$item->id] : null;
261         }
262         return $this->valuestmp;
263     }
265     /**
266      * Retrieves responses from an finished attempt.
267      *
268      * @return array the responses (from the feedback_value table)
269      * @since  Moodle 3.3
270      */
271     public function get_finished_responses() {
272         global $DB;
273         $responses = array();
275         if ($this->completed) {
276             $responses = $DB->get_records('feedback_value', ['completed' => $this->completed->id]);
277         }
278         return $responses;
279     }
281     /**
282      * Returns all completed values for this feedback or just a value for an item
283      * @param stdClass $item
284      * @return array
285      */
286     protected function get_values($item = null) {
287         global $DB;
288         if ($this->values === null) {
289             $this->values = array();
290             $responses = $this->get_finished_responses();
291             foreach ($responses as $r) {
292                 $this->values[$r->item] = $r->value;
293             }
294         }
295         if ($item) {
296             return array_key_exists($item->id, $this->values) ? $this->values[$item->id] : null;
297         }
298         return $this->values;
299     }
301     /**
302      * Splits the feedback items into pages
303      *
304      * Items that we definitely know at this stage as not applicable are excluded.
305      * Items that are dependent on something that has not yet been answered are
306      * still present, as well as items with broken dependencies.
307      *
308      * @return array array of arrays of items
309      */
310     public function get_pages() {
311         $pages = [[]]; // The first page always exists.
312         $items = $this->get_items();
313         foreach ($items as $item) {
314             if ($item->typ === 'pagebreak') {
315                 $pages[] = [];
316             } else if ($this->can_see_item($item) !== false) {
317                 $pages[count($pages) - 1][] = $item;
318             }
319         }
320         return $pages;
321     }
323     /**
324      * Returns the last page that has items with the value (i.e. not label) which have been answered
325      * as well as the first page that has items with the values that have not been answered.
326      *
327      * Either of the two return values may be null if there are no answered page or there are no
328      * unanswered pages left respectively.
329      *
330      * Two pages may not be directly following each other because there may be empty pages
331      * or pages with information texts only between them
332      *
333      * @return array array of two elements [$lastcompleted, $firstincompleted]
334      */
335     protected function get_last_completed_page() {
336         $completed = [];
337         $incompleted = [];
338         $pages = $this->get_pages();
339         foreach ($pages as $pageidx => $pageitems) {
340             foreach ($pageitems as $item) {
341                 if ($item->hasvalue) {
342                     if ($this->get_values_tmp($item) !== null) {
343                         $completed[$pageidx] = true;
344                     } else {
345                         $incompleted[$pageidx] = true;
346                     }
347                 }
348             }
349         }
350         $completed = array_keys($completed);
351         $incompleted = array_keys($incompleted);
352         // If some page has both completed and incompleted items it is considered incompleted.
353         $completed = array_diff($completed, $incompleted);
354         // If the completed page follows an incompleted page, it does not count.
355         $firstincompleted = $incompleted ? min($incompleted) : null;
356         if ($firstincompleted !== null) {
357             $completed = array_filter($completed, function($a) use ($firstincompleted) {
358                 return $a < $firstincompleted;
359             });
360         }
361         $lastcompleted = $completed ? max($completed) : null;
362         return [$lastcompleted, $firstincompleted];
363     }
365     /**
366      * Get the next page for the feedback
367      *
368      * This is normally $gopage+1 but may be bigger if there are empty pages or
369      * pages without visible questions.
370      *
371      * This method can only be called when questions on the current page are
372      * already answered, otherwise it may be inaccurate.
373      *
374      * @param int $gopage current page
375      * @param bool $strictcheck when gopage is the user-input value, make sure we do not jump over unanswered questions
376      * @return int|null the index of the next page or null if this is the last page
377      */
378     public function get_next_page($gopage, $strictcheck = true) {
379         if ($strictcheck) {
380             list($lastcompleted, $firstincompleted) = $this->get_last_completed_page();
381             if ($firstincompleted !== null && $firstincompleted <= $gopage) {
382                 return $firstincompleted;
383             }
384         }
385         $pages = $this->get_pages();
386         for ($pageidx = $gopage + 1; $pageidx < count($pages); $pageidx++) {
387             if (!empty($pages[$pageidx])) {
388                 return $pageidx;
389             }
390         }
391         // No further pages in the feedback have any visible items.
392         return null;
393     }
395     /**
396      * Get the previous page for the feedback
397      *
398      * This is normally $gopage-1 but may be smaller if there are empty pages or
399      * pages without visible questions.
400      *
401      * @param int $gopage current page
402      * @param bool $strictcheck when gopage is the user-input value, make sure we do not jump over unanswered questions
403      * @return int|null the index of the next page or null if this is the first page with items
404      */
405     public function get_previous_page($gopage, $strictcheck = true) {
406         if (!$gopage) {
407             // If we are already on the first (0) page, there is definitely no previous page.
408             return null;
409         }
410         $pages = $this->get_pages();
411         $rv = null;
412         // Iterate through previous pages and find the closest one that has any items on it.
413         for ($pageidx = $gopage - 1; $pageidx >= 0; $pageidx--) {
414             if (!empty($pages[$pageidx])) {
415                 $rv = $pageidx;
416                 break;
417             }
418         }
419         if ($rv === null) {
420             // We are on the very first page that has items.
421             return null;
422         }
423         if ($rv > 0 && $strictcheck) {
424             // Check if this page is actually not past than first incompleted page.
425             list($lastcompleted, $firstincompleted) = $this->get_last_completed_page();
426             if ($firstincompleted !== null && $firstincompleted < $rv) {
427                 return $firstincompleted;
428             }
429         }
430         return $rv;
431     }
433     /**
434      * Page index to resume the feedback
435      *
436      * When user abandones answering feedback and then comes back to it we should send him
437      * to the first page after the last page he fully completed.
438      * @return int
439      */
440     public function get_resume_page() {
441         list($lastcompleted, $firstincompleted) = $this->get_last_completed_page();
442         return $lastcompleted === null ? 0 : $this->get_next_page($lastcompleted, false);
443     }
445     /**
446      * Creates a new record in the 'feedback_completedtmp' table for the current user/guest session
447      *
448      * @return stdClass record from feedback_completedtmp or false if not found
449      */
450     protected function create_current_completed_tmp() {
451         global $DB, $USER;
452         $record = (object)['feedback' => $this->feedback->id];
453         if ($this->get_courseid()) {
454             $record->courseid = $this->get_courseid();
455         }
456         if ((isloggedin() || $USER->id != $this->userid) && !isguestuser($this->userid)) {
457             $record->userid = $this->userid;
458         } else {
459             $record->guestid = sesskey();
460         }
461         $record->timemodified = time();
462         $record->anonymous_response = $this->feedback->anonymous;
463         $id = $DB->insert_record('feedback_completedtmp', $record);
464         $this->completedtmp = $DB->get_record('feedback_completedtmp', ['id' => $id]);
465         $this->valuestmp = null;
466         return $this->completedtmp;
467     }
469     /**
470      * If user has already completed the feedback, create the temproray values from last completed attempt
471      *
472      * @return stdClass record from feedback_completedtmp or false if not found
473      */
474     public function create_completed_tmp_from_last_completed() {
475         if (!$this->get_current_completed_tmp()) {
476             $lastcompleted = $this->find_last_completed();
477             if ($lastcompleted) {
478                 $this->completedtmp = feedback_set_tmp_values($lastcompleted);
479             }
480         }
481         return $this->completedtmp;
482     }
484     /**
485      * Saves unfinished response to the temporary table
486      *
487      * This is called when user proceeds to the next/previous page in the complete form
488      * and also right after the form submit.
489      * After the form submit the {@link save_response()} is called to
490      * move response from temporary table to completion table.
491      *
492      * @param stdClass $data data from the form mod_feedback_complete_form
493      */
494     public function save_response_tmp($data) {
495         global $DB;
496         if (!$completedtmp = $this->get_current_completed_tmp()) {
497             $completedtmp = $this->create_current_completed_tmp();
498         } else {
499             $currentime = time();
500             $DB->update_record('feedback_completedtmp',
501                     ['id' => $completedtmp->id, 'timemodified' => $currentime]);
502             $completedtmp->timemodified = $currentime;
503         }
505         // Find all existing values.
506         $existingvalues = $DB->get_records_menu('feedback_valuetmp',
507                 ['completed' => $completedtmp->id], '', 'item, id');
509         // Loop through all feedback items and save the ones that are present in $data.
510         $allitems = $this->get_items();
511         foreach ($allitems as $item) {
512             if (!$item->hasvalue) {
513                 continue;
514             }
515             $keyname = $item->typ . '_' . $item->id;
516             if (!isset($data->$keyname)) {
517                 // This item is either on another page or dependency was not met - nothing to save.
518                 continue;
519             }
521             $newvalue = ['item' => $item->id, 'completed' => $completedtmp->id, 'course_id' => $completedtmp->courseid];
523             // Convert the value to string that can be stored in 'feedback_valuetmp' or 'feedback_value'.
524             $itemobj = feedback_get_item_class($item->typ);
525             $newvalue['value'] = $itemobj->create_value($data->$keyname);
527             // Update or insert the value in the 'feedback_valuetmp' table.
528             if (array_key_exists($item->id, $existingvalues)) {
529                 $newvalue['id'] = $existingvalues[$item->id];
530                 $DB->update_record('feedback_valuetmp', $newvalue);
531             } else {
532                 $DB->insert_record('feedback_valuetmp', $newvalue);
533             }
534         }
536         // Reset valuestmp cache.
537         $this->valuestmp = null;
538     }
540     /**
541      * Saves the response
542      *
543      * The form data has already been stored in the temporary table in
544      * {@link save_response_tmp()}. This function copies the values
545      * from the temporary table to the completion table.
546      * It is also responsible for sending email notifications when applicable.
547      */
548     public function save_response() {
549         global $SESSION, $DB, $USER;
551         $feedbackcompleted = $this->find_last_completed();
552         $feedbackcompletedtmp = $this->get_current_completed_tmp();
554         if (feedback_check_is_switchrole()) {
555             // We do not actually save anything if the role is switched, just delete temporary values.
556             $this->delete_completedtmp();
557             return;
558         }
560         // Save values.
561         $completedid = feedback_save_tmp_values($feedbackcompletedtmp, $feedbackcompleted);
562         $this->completed = $DB->get_record('feedback_completed', array('id' => $completedid));
564         // Send email.
565         if ($this->feedback->anonymous == FEEDBACK_ANONYMOUS_NO) {
566             feedback_send_email($this->cm, $this->feedback, $this->cm->get_course(), $this->userid, $this->completed);
567         } else {
568             feedback_send_email_anonym($this->cm, $this->feedback, $this->cm->get_course());
569         }
571         unset($SESSION->feedback->is_started);
573         // Update completion state.
574         $completion = new completion_info($this->cm->get_course());
575         if ((isloggedin() || $USER->id != $this->userid) && $completion->is_enabled($this->cm) &&
576                 $this->cm->completion == COMPLETION_TRACKING_AUTOMATIC && $this->feedback->completionsubmit) {
577             $completion->update_state($this->cm, COMPLETION_COMPLETE, $this->userid);
578         }
579     }
581     /**
582      * Deletes the temporary completed and all related temporary values
583      */
584     protected function delete_completedtmp() {
585         global $DB;
587         if ($completedtmp = $this->get_current_completed_tmp()) {
588             $DB->delete_records('feedback_valuetmp', ['completed' => $completedtmp->id]);
589             $DB->delete_records('feedback_completedtmp', ['id' => $completedtmp->id]);
590             $this->completedtmp = null;
591         }
592     }
594     /**
595      * Retrieves the last completion record for the current user
596      *
597      * @return stdClass record from feedback_completed or false if not found
598      */
599     public function find_last_completed() {
600         global $DB, $USER;
601         if ((!isloggedin() && $USER->id == $this->userid) || isguestuser($this->userid)) {
602             // Not possible to retrieve completed feedback for guests.
603             return false;
604         }
605         if ($this->is_anonymous()) {
606             // Not possible to retrieve completed anonymous feedback.
607             return false;
608         }
609         $params = array('feedback' => $this->feedback->id,
610             'userid' => $this->userid,
611             'anonymous_response' => FEEDBACK_ANONYMOUS_NO
612         );
613         if ($this->get_courseid()) {
614             $params['courseid'] = $this->get_courseid();
615         }
616         $this->completed = $DB->get_record('feedback_completed', $params);
617         return $this->completed;
618     }
620     /**
621      * Checks if user has capability to submit the feedback
622      *
623      * There is an exception for fully anonymous feedbacks when guests can complete
624      * feedback without the proper capability.
625      *
626      * This should be followed by checking {@link can_submit()} because even if
627      * user has capablity to complete, they may have already submitted feedback
628      * and can not re-submit
629      *
630      * @return bool
631      */
632     public function can_complete() {
633         global $CFG, $USER;
635         $context = context_module::instance($this->cm->id);
636         if (has_capability('mod/feedback:complete', $context, $this->userid)) {
637             return true;
638         }
640         if (!empty($CFG->feedback_allowfullanonymous)
641                     AND $this->feedback->course == SITEID
642                     AND $this->feedback->anonymous == FEEDBACK_ANONYMOUS_YES
643                     AND ((!isloggedin() && $USER->id == $this->userid) || isguestuser($this->userid))) {
644             // Guests are allowed to complete fully anonymous feedback without having 'mod/feedback:complete' capability.
645             return true;
646         }
648         return false;
649     }
651     /**
652      * Checks if user is prevented from re-submission.
653      *
654      * This must be called after {@link can_complete()}
655      *
656      * @return bool
657      */
658     public function can_submit() {
659         if ($this->get_feedback()->multiple_submit == 0 ) {
660             if ($this->is_already_submitted()) {
661                 return false;
662             }
663         }
664         return true;
665     }
667     /**
668      * Trigger module viewed event.
669      *
670      * @since Moodle 3.3
671      */
672     public function trigger_module_viewed() {
673         $event = \mod_feedback\event\course_module_viewed::create_from_record($this->feedback, $this->cm, $this->cm->get_course());
674         $event->trigger();
675     }
677     /**
678      * Mark activity viewed for completion-tracking.
679      *
680      * @since Moodle 3.3
681      */
682     public function set_module_viewed() {
683         global $CFG;
684         require_once($CFG->libdir . '/completionlib.php');
686         $completion = new completion_info($this->cm->get_course());
687         $completion->set_module_viewed($this->cm, $this->userid);
688     }
690     /**
691      * Process a page jump via the mod_feedback_complete_form.
692      *
693      * This function initializes the form and process the submission.
694      *
695      * @param  int $gopage         the current page
696      * @param  int $gopreviouspage if the user chose to go to the previous page
697      * @return string the url to redirect the user (if any)
698      * @since  Moodle 3.3
699      */
700     public function process_page($gopage, $gopreviouspage = false) {
701         global $CFG, $PAGE, $SESSION;
703         $urltogo = null;
705         // Save the form for later during the request.
706         $this->create_completed_tmp_from_last_completed();
707         $this->form = new mod_feedback_complete_form(mod_feedback_complete_form::MODE_COMPLETE,
708             $this, 'feedback_complete_form', array('gopage' => $gopage));
710         if ($this->form->is_cancelled()) {
711             // Form was cancelled - return to the course page.
712             $urltogo = course_get_url($this->courseid ?: $this->feedback->course);
713         } else if ($this->form->is_submitted() &&
714                 ($this->form->is_validated() || $gopreviouspage)) {
715             // Form was submitted (skip validation for "Previous page" button).
716             $data = $this->form->get_submitted_data();
717             if (!isset($SESSION->feedback->is_started) OR !$SESSION->feedback->is_started == true) {
718                 print_error('error', '', $CFG->wwwroot.'/course/view.php?id='.$this->courseid);
719             }
720             $this->save_response_tmp($data);
721             if (!empty($data->savevalues) || !empty($data->gonextpage)) {
722                 if (($nextpage = $this->get_next_page($gopage)) !== null) {
723                     if ($PAGE->has_set_url()) {
724                         $urltogo = new moodle_url($PAGE->url, array('gopage' => $nextpage));
725                     }
726                     $this->jumpto = $nextpage;
727                 } else {
728                     $this->save_response();
729                     if (!$this->get_feedback()->page_after_submit) {
730                         \core\notification::success(get_string('entries_saved', 'feedback'));
731                     }
732                     $this->justcompleted = true;
733                 }
734             } else if (!empty($gopreviouspage)) {
735                 $prevpage = intval($this->get_previous_page($gopage));
736                 if ($PAGE->has_set_url()) {
737                     $urltogo = new moodle_url($PAGE->url, array('gopage' => $prevpage));
738                 }
739                 $this->jumpto = $prevpage;
740             }
741         }
742         return $urltogo;
743     }
745     /**
746      * Render the form with the questions.
747      *
748      * @return string the form rendered
749      * @since Moodle 3.3
750      */
751     public function render_items() {
752         global $SESSION;
754         // Print the items.
755         $SESSION->feedback->is_started = true;
756         return $this->form->render();
757     }