Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]

   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   * 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   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  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;
  51  
  52  
  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;
  72  
  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      }
  98  
  99      /**
 100       * Returns a record from 'feedback_completed' table
 101       * @return stdClass
 102       */
 103      public function get_completed() {
 104          return $this->completed;
 105      }
 106  
 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      }
 116  
 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      }
 126  
 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      }
 148  
 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          $check = $itemobj->compare_value($ditem, $value, $item->dependvalue) ? true : false;
 182          if ($check) {
 183              return $this->can_see_item($ditem);
 184          }
 185          return false;
 186      }
 187  
 188      /**
 189       * Dependency condition has an error
 190       * @param stdClass $item
 191       * @return bool
 192       */
 193      protected function dependency_has_error($item) {
 194          if (empty($item->dependitem)) {
 195              // No dependency - no error.
 196              return false;
 197          }
 198          $allitems = $this->get_items();
 199          if (!array_key_exists($item->dependitem, $allitems)) {
 200              // Looks like dependent item has been removed.
 201              return true;
 202          }
 203          $itemids = array_keys($allitems);
 204          $index1 = array_search($item->dependitem, $itemids);
 205          $index2 = array_search($item->id, $itemids);
 206          if ($index1 >= $index2) {
 207              // Dependent item is after the current item in the feedback.
 208              return true;
 209          }
 210          for ($i = $index1 + 1; $i < $index2; $i++) {
 211              if ($allitems[$itemids[$i]]->typ === 'pagebreak') {
 212                  return false;
 213              }
 214          }
 215          // There are no page breaks between dependent items.
 216          return true;
 217      }
 218  
 219      /**
 220       * Returns a value stored for this item in the feedback (temporary or not, depending on the mode)
 221       * @param stdClass $item
 222       * @return string
 223       */
 224      public function get_item_value($item) {
 225          if ($this->iscompleted) {
 226              return $this->get_values($item);
 227          } else {
 228              return $this->get_values_tmp($item);
 229          }
 230      }
 231  
 232      /**
 233       * Retrieves responses from an unfinished attempt.
 234       *
 235       * @return array the responses (from the feedback_valuetmp table)
 236       * @since  Moodle 3.3
 237       */
 238      public function get_unfinished_responses() {
 239          global $DB;
 240          $responses = array();
 241  
 242          $completedtmp = $this->get_current_completed_tmp();
 243          if ($completedtmp) {
 244              $responses = $DB->get_records('feedback_valuetmp', ['completed' => $completedtmp->id]);
 245          }
 246          return $responses;
 247      }
 248  
 249      /**
 250       * Returns all temporary values for this feedback or just a value for an item
 251       * @param stdClass $item
 252       * @return array
 253       */
 254      protected function get_values_tmp($item = null) {
 255          global $DB;
 256          if ($this->valuestmp === null) {
 257              $this->valuestmp = array();
 258              $responses = $this->get_unfinished_responses();
 259              foreach ($responses as $r) {
 260                  $this->valuestmp[$r->item] = $r->value;
 261              }
 262          }
 263          if ($item) {
 264              return array_key_exists($item->id, $this->valuestmp) ? $this->valuestmp[$item->id] : null;
 265          }
 266          return $this->valuestmp;
 267      }
 268  
 269      /**
 270       * Retrieves responses from an finished attempt.
 271       *
 272       * @return array the responses (from the feedback_value table)
 273       * @since  Moodle 3.3
 274       */
 275      public function get_finished_responses() {
 276          global $DB;
 277          $responses = array();
 278  
 279          if ($this->completed) {
 280              $responses = $DB->get_records('feedback_value', ['completed' => $this->completed->id]);
 281          }
 282          return $responses;
 283      }
 284  
 285      /**
 286       * Returns all completed values for this feedback or just a value for an item
 287       * @param stdClass $item
 288       * @return array
 289       */
 290      protected function get_values($item = null) {
 291          global $DB;
 292          if ($this->values === null) {
 293              $this->values = array();
 294              $responses = $this->get_finished_responses();
 295              foreach ($responses as $r) {
 296                  $this->values[$r->item] = $r->value;
 297              }
 298          }
 299          if ($item) {
 300              return array_key_exists($item->id, $this->values) ? $this->values[$item->id] : null;
 301          }
 302          return $this->values;
 303      }
 304  
 305      /**
 306       * Splits the feedback items into pages
 307       *
 308       * Items that we definitely know at this stage as not applicable are excluded.
 309       * Items that are dependent on something that has not yet been answered are
 310       * still present, as well as items with broken dependencies.
 311       *
 312       * @return array array of arrays of items
 313       */
 314      public function get_pages() {
 315          $pages = [[]]; // The first page always exists.
 316          $items = $this->get_items();
 317          foreach ($items as $item) {
 318              if ($item->typ === 'pagebreak') {
 319                  $pages[] = [];
 320              } else if ($this->can_see_item($item) !== false) {
 321                  $pages[count($pages) - 1][] = $item;
 322              }
 323          }
 324          return $pages;
 325      }
 326  
 327      /**
 328       * Returns the last page that has items with the value (i.e. not label) which have been answered
 329       * as well as the first page that has items with the values that have not been answered.
 330       *
 331       * Either of the two return values may be null if there are no answered page or there are no
 332       * unanswered pages left respectively.
 333       *
 334       * Two pages may not be directly following each other because there may be empty pages
 335       * or pages with information texts only between them
 336       *
 337       * @return array array of two elements [$lastcompleted, $firstincompleted]
 338       */
 339      protected function get_last_completed_page() {
 340          $completed = [];
 341          $incompleted = [];
 342          $pages = $this->get_pages();
 343          foreach ($pages as $pageidx => $pageitems) {
 344              foreach ($pageitems as $item) {
 345                  if ($item->hasvalue) {
 346                      if ($this->get_values_tmp($item) !== null) {
 347                          $completed[$pageidx] = true;
 348                      } else {
 349                          $incompleted[$pageidx] = true;
 350                      }
 351                  }
 352              }
 353          }
 354          $completed = array_keys($completed);
 355          $incompleted = array_keys($incompleted);
 356          // If some page has both completed and incompleted items it is considered incompleted.
 357          $completed = array_diff($completed, $incompleted);
 358          // If the completed page follows an incompleted page, it does not count.
 359          $firstincompleted = $incompleted ? min($incompleted) : null;
 360          if ($firstincompleted !== null) {
 361              $completed = array_filter($completed, function($a) use ($firstincompleted) {
 362                  return $a < $firstincompleted;
 363              });
 364          }
 365          $lastcompleted = $completed ? max($completed) : null;
 366          return [$lastcompleted, $firstincompleted];
 367      }
 368  
 369      /**
 370       * Get the next page for the feedback
 371       *
 372       * This is normally $gopage+1 but may be bigger if there are empty pages or
 373       * pages without visible questions.
 374       *
 375       * This method can only be called when questions on the current page are
 376       * already answered, otherwise it may be inaccurate.
 377       *
 378       * @param int $gopage current page
 379       * @param bool $strictcheck when gopage is the user-input value, make sure we do not jump over unanswered questions
 380       * @return int|null the index of the next page or null if this is the last page
 381       */
 382      public function get_next_page($gopage, $strictcheck = true) {
 383          if ($strictcheck) {
 384              list($lastcompleted, $firstincompleted) = $this->get_last_completed_page();
 385              if ($firstincompleted !== null && $firstincompleted <= $gopage) {
 386                  return $firstincompleted;
 387              }
 388          }
 389          $pages = $this->get_pages();
 390          for ($pageidx = $gopage + 1; $pageidx < count($pages); $pageidx++) {
 391              if (!empty($pages[$pageidx])) {
 392                  return $pageidx;
 393              }
 394          }
 395          // No further pages in the feedback have any visible items.
 396          return null;
 397      }
 398  
 399      /**
 400       * Get the previous page for the feedback
 401       *
 402       * This is normally $gopage-1 but may be smaller if there are empty pages or
 403       * pages without visible questions.
 404       *
 405       * @param int $gopage current page
 406       * @param bool $strictcheck when gopage is the user-input value, make sure we do not jump over unanswered questions
 407       * @return int|null the index of the next page or null if this is the first page with items
 408       */
 409      public function get_previous_page($gopage, $strictcheck = true) {
 410          if (!$gopage) {
 411              // If we are already on the first (0) page, there is definitely no previous page.
 412              return null;
 413          }
 414          $pages = $this->get_pages();
 415          $rv = null;
 416          // Iterate through previous pages and find the closest one that has any items on it.
 417          for ($pageidx = $gopage - 1; $pageidx >= 0; $pageidx--) {
 418              if (!empty($pages[$pageidx])) {
 419                  $rv = $pageidx;
 420                  break;
 421              }
 422          }
 423          if ($rv === null) {
 424              // We are on the very first page that has items.
 425              return null;
 426          }
 427          if ($rv > 0 && $strictcheck) {
 428              // Check if this page is actually not past than first incompleted page.
 429              list($lastcompleted, $firstincompleted) = $this->get_last_completed_page();
 430              if ($firstincompleted !== null && $firstincompleted < $rv) {
 431                  return $firstincompleted;
 432              }
 433          }
 434          return $rv;
 435      }
 436  
 437      /**
 438       * Page index to resume the feedback
 439       *
 440       * When user abandones answering feedback and then comes back to it we should send him
 441       * to the first page after the last page he fully completed.
 442       * @return int
 443       */
 444      public function get_resume_page() {
 445          list($lastcompleted, $firstincompleted) = $this->get_last_completed_page();
 446          return $lastcompleted === null ? 0 : $this->get_next_page($lastcompleted, false);
 447      }
 448  
 449      /**
 450       * Creates a new record in the 'feedback_completedtmp' table for the current user/guest session
 451       *
 452       * @return stdClass record from feedback_completedtmp or false if not found
 453       */
 454      protected function create_current_completed_tmp() {
 455          global $DB, $USER;
 456          $record = (object)['feedback' => $this->feedback->id];
 457          if ($this->get_courseid()) {
 458              $record->courseid = $this->get_courseid();
 459          }
 460          if ((isloggedin() || $USER->id != $this->userid) && !isguestuser($this->userid)) {
 461              $record->userid = $this->userid;
 462          } else {
 463              $record->guestid = sesskey();
 464          }
 465          $record->timemodified = time();
 466          $record->anonymous_response = $this->feedback->anonymous;
 467          $id = $DB->insert_record('feedback_completedtmp', $record);
 468          $this->completedtmp = $DB->get_record('feedback_completedtmp', ['id' => $id]);
 469          $this->valuestmp = null;
 470          return $this->completedtmp;
 471      }
 472  
 473      /**
 474       * If user has already completed the feedback, create the temproray values from last completed attempt
 475       *
 476       * @return stdClass record from feedback_completedtmp or false if not found
 477       */
 478      public function create_completed_tmp_from_last_completed() {
 479          if (!$this->get_current_completed_tmp()) {
 480              $lastcompleted = $this->find_last_completed();
 481              if ($lastcompleted) {
 482                  $this->completedtmp = feedback_set_tmp_values($lastcompleted);
 483              }
 484          }
 485          return $this->completedtmp;
 486      }
 487  
 488      /**
 489       * Saves unfinished response to the temporary table
 490       *
 491       * This is called when user proceeds to the next/previous page in the complete form
 492       * and also right after the form submit.
 493       * After the form submit the {@link save_response()} is called to
 494       * move response from temporary table to completion table.
 495       *
 496       * @param stdClass $data data from the form mod_feedback_complete_form
 497       */
 498      public function save_response_tmp($data) {
 499          global $DB;
 500          if (!$completedtmp = $this->get_current_completed_tmp()) {
 501              $completedtmp = $this->create_current_completed_tmp();
 502          } else {
 503              $currentime = time();
 504              $DB->update_record('feedback_completedtmp',
 505                      ['id' => $completedtmp->id, 'timemodified' => $currentime]);
 506              $completedtmp->timemodified = $currentime;
 507          }
 508  
 509          // Find all existing values.
 510          $existingvalues = $DB->get_records_menu('feedback_valuetmp',
 511                  ['completed' => $completedtmp->id], '', 'item, id');
 512  
 513          // Loop through all feedback items and save the ones that are present in $data.
 514          $allitems = $this->get_items();
 515          foreach ($allitems as $item) {
 516              if (!$item->hasvalue) {
 517                  continue;
 518              }
 519              $keyname = $item->typ . '_' . $item->id;
 520              if (!isset($data->$keyname)) {
 521                  // This item is either on another page or dependency was not met - nothing to save.
 522                  continue;
 523              }
 524  
 525              $newvalue = ['item' => $item->id, 'completed' => $completedtmp->id, 'course_id' => $completedtmp->courseid];
 526  
 527              // Convert the value to string that can be stored in 'feedback_valuetmp' or 'feedback_value'.
 528              $itemobj = feedback_get_item_class($item->typ);
 529              $newvalue['value'] = $itemobj->create_value($data->$keyname);
 530  
 531              // Update or insert the value in the 'feedback_valuetmp' table.
 532              if (array_key_exists($item->id, $existingvalues)) {
 533                  $newvalue['id'] = $existingvalues[$item->id];
 534                  $DB->update_record('feedback_valuetmp', $newvalue);
 535              } else {
 536                  $DB->insert_record('feedback_valuetmp', $newvalue);
 537              }
 538          }
 539  
 540          // Reset valuestmp cache.
 541          $this->valuestmp = null;
 542      }
 543  
 544      /**
 545       * Saves the response
 546       *
 547       * The form data has already been stored in the temporary table in
 548       * {@link save_response_tmp()}. This function copies the values
 549       * from the temporary table to the completion table.
 550       * It is also responsible for sending email notifications when applicable.
 551       */
 552      public function save_response() {
 553          global $SESSION, $DB, $USER;
 554  
 555          $feedbackcompleted = $this->find_last_completed();
 556          $feedbackcompletedtmp = $this->get_current_completed_tmp();
 557  
 558          if (feedback_check_is_switchrole()) {
 559              // We do not actually save anything if the role is switched, just delete temporary values.
 560              $this->delete_completedtmp();
 561              return;
 562          }
 563  
 564          // Save values.
 565          $completedid = feedback_save_tmp_values($feedbackcompletedtmp, $feedbackcompleted);
 566          $this->completed = $DB->get_record('feedback_completed', array('id' => $completedid));
 567  
 568          // Send email.
 569          if ($this->feedback->anonymous == FEEDBACK_ANONYMOUS_NO) {
 570              feedback_send_email($this->cm, $this->feedback, $this->cm->get_course(), $this->userid, $this->completed);
 571          } else {
 572              feedback_send_email_anonym($this->cm, $this->feedback, $this->cm->get_course());
 573          }
 574  
 575          unset($SESSION->feedback->is_started);
 576  
 577          // Update completion state.
 578          $completion = new completion_info($this->cm->get_course());
 579          if ((isloggedin() || $USER->id != $this->userid) && $completion->is_enabled($this->cm) &&
 580                  $this->cm->completion == COMPLETION_TRACKING_AUTOMATIC && $this->feedback->completionsubmit) {
 581              $completion->update_state($this->cm, COMPLETION_COMPLETE, $this->userid);
 582          }
 583      }
 584  
 585      /**
 586       * Deletes the temporary completed and all related temporary values
 587       */
 588      protected function delete_completedtmp() {
 589          global $DB;
 590  
 591          if ($completedtmp = $this->get_current_completed_tmp()) {
 592              $DB->delete_records('feedback_valuetmp', ['completed' => $completedtmp->id]);
 593              $DB->delete_records('feedback_completedtmp', ['id' => $completedtmp->id]);
 594              $this->completedtmp = null;
 595          }
 596      }
 597  
 598      /**
 599       * Retrieves the last completion record for the current user
 600       *
 601       * @return stdClass record from feedback_completed or false if not found
 602       */
 603      public function find_last_completed() {
 604          global $DB, $USER;
 605          if ((!isloggedin() && $USER->id == $this->userid) || isguestuser($this->userid)) {
 606              // Not possible to retrieve completed feedback for guests.
 607              return false;
 608          }
 609          if ($this->is_anonymous()) {
 610              // Not possible to retrieve completed anonymous feedback.
 611              return false;
 612          }
 613          $params = array('feedback' => $this->feedback->id,
 614              'userid' => $this->userid,
 615              'anonymous_response' => FEEDBACK_ANONYMOUS_NO
 616          );
 617          if ($this->get_courseid()) {
 618              $params['courseid'] = $this->get_courseid();
 619          }
 620          $this->completed = $DB->get_record('feedback_completed', $params);
 621          return $this->completed;
 622      }
 623  
 624      /**
 625       * Checks if user has capability to submit the feedback
 626       *
 627       * There is an exception for fully anonymous feedbacks when guests can complete
 628       * feedback without the proper capability.
 629       *
 630       * This should be followed by checking {@link can_submit()} because even if
 631       * user has capablity to complete, they may have already submitted feedback
 632       * and can not re-submit
 633       *
 634       * @return bool
 635       */
 636      public function can_complete() {
 637          global $CFG, $USER;
 638  
 639          $context = context_module::instance($this->cm->id);
 640          if (has_capability('mod/feedback:complete', $context, $this->userid)) {
 641              return true;
 642          }
 643  
 644          if (!empty($CFG->feedback_allowfullanonymous)
 645                      AND $this->feedback->course == SITEID
 646                      AND $this->feedback->anonymous == FEEDBACK_ANONYMOUS_YES
 647                      AND ((!isloggedin() && $USER->id == $this->userid) || isguestuser($this->userid))) {
 648              // Guests are allowed to complete fully anonymous feedback without having 'mod/feedback:complete' capability.
 649              return true;
 650          }
 651  
 652          return false;
 653      }
 654  
 655      /**
 656       * Checks if user is prevented from re-submission.
 657       *
 658       * This must be called after {@link can_complete()}
 659       *
 660       * @return bool
 661       */
 662      public function can_submit() {
 663          if ($this->get_feedback()->multiple_submit == 0 ) {
 664              if ($this->is_already_submitted()) {
 665                  return false;
 666              }
 667          }
 668          return true;
 669      }
 670  
 671      /**
 672       * Trigger module viewed event.
 673       *
 674       * @since Moodle 3.3
 675       */
 676      public function trigger_module_viewed() {
 677          $event = \mod_feedback\event\course_module_viewed::create_from_record($this->feedback, $this->cm, $this->cm->get_course());
 678          $event->trigger();
 679      }
 680  
 681      /**
 682       * Mark activity viewed for completion-tracking.
 683       *
 684       * @since Moodle 3.3
 685       */
 686      public function set_module_viewed() {
 687          global $CFG;
 688          require_once($CFG->libdir . '/completionlib.php');
 689  
 690          $completion = new completion_info($this->cm->get_course());
 691          $completion->set_module_viewed($this->cm, $this->userid);
 692      }
 693  
 694      /**
 695       * Process a page jump via the mod_feedback_complete_form.
 696       *
 697       * This function initializes the form and process the submission.
 698       *
 699       * @param  int $gopage         the current page
 700       * @param  int $gopreviouspage if the user chose to go to the previous page
 701       * @return string the url to redirect the user (if any)
 702       * @since  Moodle 3.3
 703       */
 704      public function process_page($gopage, $gopreviouspage = false) {
 705          global $CFG, $PAGE, $SESSION;
 706  
 707          $urltogo = null;
 708  
 709          // Save the form for later during the request.
 710          $this->create_completed_tmp_from_last_completed();
 711          $this->form = new mod_feedback_complete_form(mod_feedback_complete_form::MODE_COMPLETE,
 712              $this, 'feedback_complete_form', array('gopage' => $gopage));
 713  
 714          if ($this->form->is_cancelled()) {
 715              // Form was cancelled - return to the course page.
 716              $urltogo = new moodle_url('/mod/feedback/view.php', ['id' => $this->get_cm()->id]);
 717          } else if ($this->form->is_submitted() &&
 718                  ($this->form->is_validated() || $gopreviouspage)) {
 719              // Form was submitted (skip validation for "Previous page" button).
 720              $data = $this->form->get_submitted_data();
 721              if (!isset($SESSION->feedback->is_started) OR !$SESSION->feedback->is_started == true) {
 722                  throw new \moodle_exception('error', '', $CFG->wwwroot.'/course/view.php?id='.$this->courseid);
 723              }
 724              $this->save_response_tmp($data);
 725              if (!empty($data->savevalues) || !empty($data->gonextpage)) {
 726                  if (($nextpage = $this->get_next_page($gopage)) !== null) {
 727                      if ($PAGE->has_set_url()) {
 728                          $urltogo = new moodle_url($PAGE->url, array('gopage' => $nextpage));
 729                      }
 730                      $this->jumpto = $nextpage;
 731                  } else {
 732                      $this->save_response();
 733                      if (!$this->get_feedback()->page_after_submit) {
 734                          \core\notification::success(get_string('entries_saved', 'feedback'));
 735                      }
 736                      $this->justcompleted = true;
 737                  }
 738              } else if (!empty($gopreviouspage)) {
 739                  $prevpage = intval($this->get_previous_page($gopage));
 740                  if ($PAGE->has_set_url()) {
 741                      $urltogo = new moodle_url($PAGE->url, array('gopage' => $prevpage));
 742                  }
 743                  $this->jumpto = $prevpage;
 744              }
 745          }
 746          return $urltogo;
 747      }
 748  
 749      /**
 750       * Render the form with the questions.
 751       *
 752       * @return string the form rendered
 753       * @since Moodle 3.3
 754       */
 755      public function render_items() {
 756          global $SESSION;
 757  
 758          // Print the items.
 759          $SESSION->feedback->is_started = true;
 760          return $this->form->render();
 761      }
 762  }