[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |