[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/mod/feedback/classes/ -> completion.php (source)

   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  
  46  
  47      /**
  48       * Constructor
  49       *
  50       * @param stdClass $feedback feedback object, in case of the template
  51       *     this is the current feedback the template is accessed from
  52       * @param cm_info $cm course module object corresponding to the $feedback
  53       * @param int $courseid current course (for site feedbacks only)
  54       * @param bool $iscompleted has feedback been already completed? If yes either completedid or userid must be specified.
  55       * @param int $completedid id in the table feedback_completed, may be omitted if userid is specified
  56       *     but it is highly recommended because the same user may have multiple responses to the same feedback
  57       *     for different courses
  58       * @param int $userid id of the user - if specified only non-anonymous replies will be returned. If not
  59       *     specified only anonymous replies will be returned and the $completedid is mandatory.
  60       */
  61      public function __construct($feedback, $cm, $courseid, $iscompleted = false, $completedid = null, $userid = null) {
  62          global $DB;
  63          // Make sure courseid is always set for site feedback and never for course feedback.
  64          if ($feedback->course == SITEID) {
  65              $courseid = $courseid ?: SITEID;
  66          } else {
  67              $courseid = 0;
  68          }
  69          parent::__construct($feedback, $cm, $courseid, 0);
  70          if ($iscompleted) {
  71              // Retrieve information about the completion.
  72              $this->iscompleted = true;
  73              $params = array('feedback' => $feedback->id);
  74              if (!$userid && !$completedid) {
  75                  throw new coding_exception('Either $completedid or $userid must be specified for completed feedbacks');
  76              }
  77              if ($completedid) {
  78                  $params['id'] = $completedid;
  79              }
  80              if ($userid) {
  81                  // We must respect the anonymousity of the reply that the user saw when they were completing the feedback,
  82                  // not the current state that may have been changed later by the teacher.
  83                  $params['anonymous_response'] = FEEDBACK_ANONYMOUS_NO;
  84                  $params['userid'] = $userid;
  85              }
  86              $this->completed = $DB->get_record('feedback_completed', $params, '*', MUST_EXIST);
  87              $this->courseid = $this->completed->courseid;
  88          }
  89      }
  90  
  91      /**
  92       * Returns a record from 'feedback_completed' table
  93       * @return stdClass
  94       */
  95      public function get_completed() {
  96          return $this->completed;
  97      }
  98  
  99      /**
 100       * Returns the temporary completion record for the current user or guest session
 101       *
 102       * @return stdClass|false record from feedback_completedtmp or false if not found
 103       */
 104      protected function get_current_completed_tmp() {
 105          global $USER, $DB;
 106          if ($this->completedtmp === null) {
 107              $params = array('feedback' => $this->get_feedback()->id);
 108              if ($courseid = $this->get_courseid()) {
 109                  $params['courseid'] = $courseid;
 110              }
 111              if (isloggedin() && !isguestuser()) {
 112                  $params['userid'] = $USER->id;
 113              } else {
 114                  $params['guestid'] = sesskey();
 115              }
 116              $this->completedtmp = $DB->get_record('feedback_completedtmp', $params);
 117          }
 118          return $this->completedtmp;
 119      }
 120  
 121      /**
 122       * Can the current user see the item, if dependency is met?
 123       *
 124       * @param stdClass $item
 125       * @return bool whether user can see item or not,
 126       *     null if dependency is broken or dependent question is not answered.
 127       */
 128      protected function can_see_item($item) {
 129          if (empty($item->dependitem)) {
 130              return true;
 131          }
 132          if ($this->dependency_has_error($item)) {
 133              return null;
 134          }
 135          $allitems = $this->get_items();
 136          $ditem = $allitems[$item->dependitem];
 137          $itemobj = feedback_get_item_class($ditem->typ);
 138          if ($this->iscompleted) {
 139              $value = $this->get_values($ditem);
 140          } else {
 141              $value = $this->get_values_tmp($ditem);
 142          }
 143          if ($value === null) {
 144              return null;
 145          }
 146          return $itemobj->compare_value($ditem, $value, $item->dependvalue) ? true : false;
 147      }
 148  
 149      /**
 150       * Dependency condition has an error
 151       * @param stdClass $item
 152       * @return bool
 153       */
 154      protected function dependency_has_error($item) {
 155          if (empty($item->dependitem)) {
 156              // No dependency - no error.
 157              return false;
 158          }
 159          $allitems = $this->get_items();
 160          if (!array_key_exists($item->dependitem, $allitems)) {
 161              // Looks like dependent item has been removed.
 162              return true;
 163          }
 164          $itemids = array_keys($allitems);
 165          $index1 = array_search($item->dependitem, $itemids);
 166          $index2 = array_search($item->id, $itemids);
 167          if ($index1 >= $index2) {
 168              // Dependent item is after the current item in the feedback.
 169              return true;
 170          }
 171          for ($i = $index1 + 1; $i < $index2; $i++) {
 172              if ($allitems[$itemids[$i]]->typ === 'pagebreak') {
 173                  return false;
 174              }
 175          }
 176          // There are no page breaks between dependent items.
 177          return true;
 178      }
 179  
 180      /**
 181       * Returns a value stored for this item in the feedback (temporary or not, depending on the mode)
 182       * @param stdClass $item
 183       * @return string
 184       */
 185      public function get_item_value($item) {
 186          if ($this->iscompleted) {
 187              return $this->get_values($item);
 188          } else {
 189              return $this->get_values_tmp($item);
 190          }
 191      }
 192  
 193      /**
 194       * Returns all temporary values for this feedback or just a value for an item
 195       * @param stdClass $item
 196       * @return array
 197       */
 198      protected function get_values_tmp($item = null) {
 199          global $DB;
 200          if ($this->valuestmp === null) {
 201              $completedtmp = $this->get_current_completed_tmp();
 202              if ($completedtmp) {
 203                  $this->valuestmp = $DB->get_records_menu('feedback_valuetmp',
 204                          ['completed' => $completedtmp->id], '', 'item, value');
 205              } else {
 206                  $this->valuestmp = array();
 207              }
 208          }
 209          if ($item) {
 210              return array_key_exists($item->id, $this->valuestmp) ? $this->valuestmp[$item->id] : null;
 211          }
 212          return $this->valuestmp;
 213      }
 214  
 215      /**
 216       * Returns all completed values for this feedback or just a value for an item
 217       * @param stdClass $item
 218       * @return array
 219       */
 220      protected function get_values($item = null) {
 221          global $DB;
 222          if ($this->values === null) {
 223              if ($this->completed) {
 224                  $this->values = $DB->get_records_menu('feedback_value',
 225                          ['completed' => $this->completed->id], '', 'item, value');
 226              } else {
 227                  $this->values = array();
 228              }
 229          }
 230          if ($item) {
 231              return array_key_exists($item->id, $this->values) ? $this->values[$item->id] : null;
 232          }
 233          return $this->values;
 234      }
 235  
 236      /**
 237       * Splits the feedback items into pages
 238       *
 239       * Items that we definitely know at this stage as not applicable are excluded.
 240       * Items that are dependent on something that has not yet been answered are
 241       * still present, as well as items with broken dependencies.
 242       *
 243       * @return array array of arrays of items
 244       */
 245      public function get_pages() {
 246          $pages = [[]]; // The first page always exists.
 247          $items = $this->get_items();
 248          foreach ($items as $item) {
 249              if ($item->typ === 'pagebreak') {
 250                  $pages[] = [];
 251              } else if ($this->can_see_item($item) !== false) {
 252                  $pages[count($pages) - 1][] = $item;
 253              }
 254          }
 255          return $pages;
 256      }
 257  
 258      /**
 259       * Returns the last page that has items with the value (i.e. not label) which have been answered
 260       * as well as the first page that has items with the values that have not been answered.
 261       *
 262       * Either of the two return values may be null if there are no answered page or there are no
 263       * unanswered pages left respectively.
 264       *
 265       * Two pages may not be directly following each other because there may be empty pages
 266       * or pages with information texts only between them
 267       *
 268       * @return array array of two elements [$lastcompleted, $firstincompleted]
 269       */
 270      protected function get_last_completed_page() {
 271          $completed = [];
 272          $incompleted = [];
 273          $pages = $this->get_pages();
 274          foreach ($pages as $pageidx => $pageitems) {
 275              foreach ($pageitems as $item) {
 276                  if ($item->hasvalue) {
 277                      if ($this->get_values_tmp($item) !== null) {
 278                          $completed[$pageidx] = true;
 279                      } else {
 280                          $incompleted[$pageidx] = true;
 281                      }
 282                  }
 283              }
 284          }
 285          $completed = array_keys($completed);
 286          $incompleted = array_keys($incompleted);
 287          // If some page has both completed and incompleted items it is considered incompleted.
 288          $completed = array_diff($completed, $incompleted);
 289          // If the completed page follows an incompleted page, it does not count.
 290          $firstincompleted = $incompleted ? min($incompleted) : null;
 291          if ($firstincompleted !== null) {
 292              $completed = array_filter($completed, function($a) use ($firstincompleted) {
 293                  return $a < $firstincompleted;
 294              });
 295          }
 296          $lastcompleted = $completed ? max($completed) : null;
 297          return [$lastcompleted, $firstincompleted];
 298      }
 299  
 300      /**
 301       * Get the next page for the feedback
 302       *
 303       * This is normally $gopage+1 but may be bigger if there are empty pages or
 304       * pages without visible questions.
 305       *
 306       * This method can only be called when questions on the current page are
 307       * already answered, otherwise it may be inaccurate.
 308       *
 309       * @param int $gopage current page
 310       * @param bool $strictcheck when gopage is the user-input value, make sure we do not jump over unanswered questions
 311       * @return int|null the index of the next page or null if this is the last page
 312       */
 313      public function get_next_page($gopage, $strictcheck = true) {
 314          if ($strictcheck) {
 315              list($lastcompleted, $firstincompleted) = $this->get_last_completed_page();
 316              if ($firstincompleted !== null && $firstincompleted <= $gopage) {
 317                  return $firstincompleted;
 318              }
 319          }
 320          $pages = $this->get_pages();
 321          for ($pageidx = $gopage + 1; $pageidx < count($pages); $pageidx++) {
 322              if (!empty($pages[$pageidx])) {
 323                  return $pageidx;
 324              }
 325          }
 326          // No further pages in the feedback have any visible items.
 327          return null;
 328      }
 329  
 330      /**
 331       * Get the previous page for the feedback
 332       *
 333       * This is normally $gopage-1 but may be smaller if there are empty pages or
 334       * pages without visible questions.
 335       *
 336       * @param int $gopage current page
 337       * @param bool $strictcheck when gopage is the user-input value, make sure we do not jump over unanswered questions
 338       * @return int|null the index of the next page or null if this is the first page with items
 339       */
 340      public function get_previous_page($gopage, $strictcheck = true) {
 341          if (!$gopage) {
 342              // If we are already on the first (0) page, there is definitely no previous page.
 343              return null;
 344          }
 345          $pages = $this->get_pages();
 346          $rv = null;
 347          // Iterate through previous pages and find the closest one that has any items on it.
 348          for ($pageidx = $gopage - 1; $pageidx >= 0; $pageidx--) {
 349              if (!empty($pages[$pageidx])) {
 350                  $rv = $pageidx;
 351                  break;
 352              }
 353          }
 354          if ($rv === null) {
 355              // We are on the very first page that has items.
 356              return null;
 357          }
 358          if ($rv > 0 && $strictcheck) {
 359              // Check if this page is actually not past than first incompleted page.
 360              list($lastcompleted, $firstincompleted) = $this->get_last_completed_page();
 361              if ($firstincompleted !== null && $firstincompleted < $rv) {
 362                  return $firstincompleted;
 363              }
 364          }
 365          return $rv;
 366      }
 367  
 368      /**
 369       * Page index to resume the feedback
 370       *
 371       * When user abandones answering feedback and then comes back to it we should send him
 372       * to the first page after the last page he fully completed.
 373       * @return int
 374       */
 375      public function get_resume_page() {
 376          list($lastcompleted, $firstincompleted) = $this->get_last_completed_page();
 377          return $lastcompleted === null ? 0 : $this->get_next_page($lastcompleted, false);
 378      }
 379  
 380      /**
 381       * Creates a new record in the 'feedback_completedtmp' table for the current user/guest session
 382       *
 383       * @return stdClass record from feedback_completedtmp or false if not found
 384       */
 385      protected function create_current_completed_tmp() {
 386          global $USER, $DB;
 387          $record = (object)['feedback' => $this->feedback->id];
 388          if ($this->get_courseid()) {
 389              $record->courseid = $this->get_courseid();
 390          }
 391          if (isloggedin() && !isguestuser()) {
 392              $record->userid = $USER->id;
 393          } else {
 394              $record->guestid = sesskey();
 395          }
 396          $record->timemodified = time();
 397          $record->anonymous_response = $this->feedback->anonymous;
 398          $id = $DB->insert_record('feedback_completedtmp', $record);
 399          $this->completedtmp = $DB->get_record('feedback_completedtmp', ['id' => $id]);
 400          $this->valuestmp = null;
 401          return $this->completedtmp;
 402      }
 403  
 404      /**
 405       * Saves unfinished response to the temporary table
 406       *
 407       * This is called when user proceeds to the next/previous page in the complete form
 408       * and also right after the form submit.
 409       * After the form submit the {@link save_response()} is called to
 410       * move response from temporary table to completion table.
 411       *
 412       * @param stdClass $data data from the form mod_feedback_complete_form
 413       */
 414      public function save_response_tmp($data) {
 415          global $DB;
 416          if (!$completedtmp = $this->get_current_completed_tmp()) {
 417              $completedtmp = $this->create_current_completed_tmp();
 418          } else {
 419              $currentime = time();
 420              $DB->update_record('feedback_completedtmp',
 421                      ['id' => $completedtmp->id, 'timemodified' => $currentime]);
 422              $completedtmp->timemodified = $currentime;
 423          }
 424  
 425          // Find all existing values.
 426          $existingvalues = $DB->get_records_menu('feedback_valuetmp',
 427                  ['completed' => $completedtmp->id], '', 'item, id');
 428  
 429          // Loop through all feedback items and save the ones that are present in $data.
 430          $allitems = $this->get_items();
 431          foreach ($allitems as $item) {
 432              if (!$item->hasvalue) {
 433                  continue;
 434              }
 435              $keyname = $item->typ . '_' . $item->id;
 436              if (!isset($data->$keyname)) {
 437                  // This item is either on another page or dependency was not met - nothing to save.
 438                  continue;
 439              }
 440  
 441              $newvalue = ['item' => $item->id, 'completed' => $completedtmp->id, 'course_id' => $completedtmp->courseid];
 442  
 443              // Convert the value to string that can be stored in 'feedback_valuetmp' or 'feedback_value'.
 444              $itemobj = feedback_get_item_class($item->typ);
 445              $newvalue['value'] = $itemobj->create_value($data->$keyname);
 446  
 447              // Update or insert the value in the 'feedback_valuetmp' table.
 448              if (array_key_exists($item->id, $existingvalues)) {
 449                  $newvalue['id'] = $existingvalues[$item->id];
 450                  $DB->update_record('feedback_valuetmp', $newvalue);
 451              } else {
 452                  $DB->insert_record('feedback_valuetmp', $newvalue);
 453              }
 454          }
 455  
 456          // Reset valuestmp cache.
 457          $this->valuestmp = null;
 458      }
 459  
 460      /**
 461       * Saves the response
 462       *
 463       * The form data has already been stored in the temporary table in
 464       * {@link save_response_tmp()}. This function copies the values
 465       * from the temporary table to the completion table.
 466       * It is also responsible for sending email notifications when applicable.
 467       */
 468      public function save_response() {
 469          global $USER, $SESSION, $DB;
 470  
 471          $feedbackcompleted = $this->find_last_completed();
 472          $feedbackcompletedtmp = $this->get_current_completed_tmp();
 473  
 474          if (feedback_check_is_switchrole()) {
 475              // We do not actually save anything if the role is switched, just delete temporary values.
 476              $this->delete_completedtmp();
 477              return;
 478          }
 479  
 480          // Save values.
 481          $completedid = feedback_save_tmp_values($feedbackcompletedtmp, $feedbackcompleted);
 482          $this->completed = $DB->get_record('feedback_completed', array('id' => $completedid));
 483  
 484          // Send email.
 485          if ($this->feedback->anonymous == FEEDBACK_ANONYMOUS_NO) {
 486              feedback_send_email($this->cm, $this->feedback, $this->cm->get_course(), $USER);
 487          } else {
 488              feedback_send_email_anonym($this->cm, $this->feedback, $this->cm->get_course());
 489          }
 490  
 491          unset($SESSION->feedback->is_started);
 492  
 493          // Update completion state.
 494          $completion = new completion_info($this->cm->get_course());
 495          if (isloggedin() && !isguestuser() && $completion->is_enabled($this->cm) && $this->feedback->completionsubmit) {
 496              $completion->update_state($this->cm, COMPLETION_COMPLETE);
 497          }
 498      }
 499  
 500      /**
 501       * Deletes the temporary completed and all related temporary values
 502       */
 503      protected function delete_completedtmp() {
 504          global $DB;
 505  
 506          if ($completedtmp = $this->get_current_completed_tmp()) {
 507              $DB->delete_records('feedback_valuetmp', ['completed' => $completedtmp->id]);
 508              $DB->delete_records('feedback_completedtmp', ['id' => $completedtmp->id]);
 509              $this->completedtmp = null;
 510          }
 511      }
 512  
 513      /**
 514       * Retrieves the last completion record for the current user
 515       *
 516       * @return stdClass record from feedback_completed or false if not found
 517       */
 518      protected function find_last_completed() {
 519          global $USER, $DB;
 520          if (isloggedin() || isguestuser()) {
 521              // Not possible to retrieve completed feedback for guests.
 522              return false;
 523          }
 524          if ($this->is_anonymous()) {
 525              // Not possible to retrieve completed anonymous feedback.
 526              return false;
 527          }
 528          $params = array('feedback' => $this->feedback->id, 'userid' => $USER->id);
 529          if ($this->get_courseid()) {
 530              $params['courseid'] = $this->get_courseid();
 531          }
 532          $this->completed = $DB->get_record('feedback_completed', $params);
 533          return $this->completed;
 534      }
 535  
 536      /**
 537       * Checks if current user has capability to submit the feedback
 538       *
 539       * There is an exception for fully anonymous feedbacks when guests can complete
 540       * feedback without the proper capability.
 541       *
 542       * This should be followed by checking {@link can_submit()} because even if
 543       * user has capablity to complete, they may have already submitted feedback
 544       * and can not re-submit
 545       *
 546       * @return bool
 547       */
 548      public function can_complete() {
 549          global $CFG;
 550  
 551          $context = context_module::instance($this->cm->id);
 552          if (has_capability('mod/feedback:complete', $context)) {
 553              return true;
 554          }
 555  
 556          if (!empty($CFG->feedback_allowfullanonymous)
 557                      AND $this->feedback->course == SITEID
 558                      AND $this->feedback->anonymous == FEEDBACK_ANONYMOUS_YES
 559                      AND (!isloggedin() OR isguestuser())) {
 560              // Guests are allowed to complete fully anonymous feedback without having 'mod/feedback:complete' capability.
 561              return true;
 562          }
 563  
 564          return false;
 565      }
 566  
 567      /**
 568       * Checks if user is prevented from re-submission.
 569       *
 570       * This must be called after {@link can_complete()}
 571       *
 572       * @return bool
 573       */
 574      public function can_submit() {
 575          if ($this->get_feedback()->multiple_submit == 0 ) {
 576              if ($this->is_already_submitted()) {
 577                  return false;
 578              }
 579          }
 580          return true;
 581      }
 582  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1