mirror of
https://github.com/moodle/moodle.git
synced 2025-07-23 07:11:28 +02:00
582 lines
22 KiB
PHP
582 lines
22 KiB
PHP
<?php
|
|
// This file is part of Moodle - http://moodle.org/
|
|
//
|
|
// Moodle is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Moodle is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
/**
|
|
* Contains class mod_feedback_completion
|
|
*
|
|
* @package mod_feedback
|
|
* @copyright 2016 Marina Glancy
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
|
|
/**
|
|
* Collects information and methods about feedback completion (either complete.php or show_entries.php)
|
|
*
|
|
* @package mod_feedback
|
|
* @copyright 2016 Marina Glancy
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
class mod_feedback_completion extends mod_feedback_structure {
|
|
/** @var stdClass */
|
|
protected $completed;
|
|
/** @var stdClass */
|
|
protected $completedtmp = null;
|
|
/** @var stdClass[] */
|
|
protected $valuestmp = null;
|
|
/** @var stdClass[] */
|
|
protected $values = null;
|
|
/** @var bool */
|
|
protected $iscompleted = false;
|
|
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param stdClass $feedback feedback object, in case of the template
|
|
* this is the current feedback the template is accessed from
|
|
* @param cm_info $cm course module object corresponding to the $feedback
|
|
* @param int $courseid current course (for site feedbacks only)
|
|
* @param bool $iscompleted has feedback been already completed? If yes either completedid or userid must be specified.
|
|
* @param int $completedid id in the table feedback_completed, may be omitted if userid is specified
|
|
* but it is highly recommended because the same user may have multiple responses to the same feedback
|
|
* for different courses
|
|
* @param int $userid id of the user - if specified only non-anonymous replies will be returned. If not
|
|
* specified only anonymous replies will be returned and the $completedid is mandatory.
|
|
*/
|
|
public function __construct($feedback, $cm, $courseid, $iscompleted = false, $completedid = null, $userid = null) {
|
|
global $DB;
|
|
// Make sure courseid is always set for site feedback and never for course feedback.
|
|
if ($feedback->course == SITEID) {
|
|
$courseid = $courseid ?: SITEID;
|
|
} else {
|
|
$courseid = 0;
|
|
}
|
|
parent::__construct($feedback, $cm, $courseid, 0);
|
|
if ($iscompleted) {
|
|
// Retrieve information about the completion.
|
|
$this->iscompleted = true;
|
|
$params = array('feedback' => $feedback->id);
|
|
if (!$userid && !$completedid) {
|
|
throw new coding_exception('Either $completedid or $userid must be specified for completed feedbacks');
|
|
}
|
|
if ($completedid) {
|
|
$params['id'] = $completedid;
|
|
}
|
|
if ($userid) {
|
|
// We must respect the anonymousity of the reply that the user saw when they were completing the feedback,
|
|
// not the current state that may have been changed later by the teacher.
|
|
$params['anonymous_response'] = FEEDBACK_ANONYMOUS_NO;
|
|
$params['userid'] = $userid;
|
|
}
|
|
$this->completed = $DB->get_record('feedback_completed', $params, '*', MUST_EXIST);
|
|
$this->courseid = $this->completed->courseid;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a record from 'feedback_completed' table
|
|
* @return stdClass
|
|
*/
|
|
public function get_completed() {
|
|
return $this->completed;
|
|
}
|
|
|
|
/**
|
|
* Returns the temporary completion record for the current user or guest session
|
|
*
|
|
* @return stdClass|false record from feedback_completedtmp or false if not found
|
|
*/
|
|
protected function get_current_completed_tmp() {
|
|
global $USER, $DB;
|
|
if ($this->completedtmp === null) {
|
|
$params = array('feedback' => $this->get_feedback()->id);
|
|
if ($courseid = $this->get_courseid()) {
|
|
$params['courseid'] = $courseid;
|
|
}
|
|
if (isloggedin() && !isguestuser()) {
|
|
$params['userid'] = $USER->id;
|
|
} else {
|
|
$params['guestid'] = sesskey();
|
|
}
|
|
$this->completedtmp = $DB->get_record('feedback_completedtmp', $params);
|
|
}
|
|
return $this->completedtmp;
|
|
}
|
|
|
|
/**
|
|
* Can the current user see the item, if dependency is met?
|
|
*
|
|
* @param stdClass $item
|
|
* @return bool whether user can see item or not,
|
|
* null if dependency is broken or dependent question is not answered.
|
|
*/
|
|
protected function can_see_item($item) {
|
|
if (empty($item->dependitem)) {
|
|
return true;
|
|
}
|
|
if ($this->dependency_has_error($item)) {
|
|
return null;
|
|
}
|
|
$allitems = $this->get_items();
|
|
$ditem = $allitems[$item->dependitem];
|
|
$itemobj = feedback_get_item_class($ditem->typ);
|
|
if ($this->iscompleted) {
|
|
$value = $this->get_values($ditem);
|
|
} else {
|
|
$value = $this->get_values_tmp($ditem);
|
|
}
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
return $itemobj->compare_value($ditem, $value, $item->dependvalue) ? true : false;
|
|
}
|
|
|
|
/**
|
|
* Dependency condition has an error
|
|
* @param stdClass $item
|
|
* @return bool
|
|
*/
|
|
protected function dependency_has_error($item) {
|
|
if (empty($item->dependitem)) {
|
|
// No dependency - no error.
|
|
return false;
|
|
}
|
|
$allitems = $this->get_items();
|
|
if (!array_key_exists($item->dependitem, $allitems)) {
|
|
// Looks like dependent item has been removed.
|
|
return true;
|
|
}
|
|
$itemids = array_keys($allitems);
|
|
$index1 = array_search($item->dependitem, $itemids);
|
|
$index2 = array_search($item->id, $itemids);
|
|
if ($index1 >= $index2) {
|
|
// Dependent item is after the current item in the feedback.
|
|
return true;
|
|
}
|
|
for ($i = $index1 + 1; $i < $index2; $i++) {
|
|
if ($allitems[$itemids[$i]]->typ === 'pagebreak') {
|
|
return false;
|
|
}
|
|
}
|
|
// There are no page breaks between dependent items.
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns a value stored for this item in the feedback (temporary or not, depending on the mode)
|
|
* @param stdClass $item
|
|
* @return string
|
|
*/
|
|
public function get_item_value($item) {
|
|
if ($this->iscompleted) {
|
|
return $this->get_values($item);
|
|
} else {
|
|
return $this->get_values_tmp($item);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns all temporary values for this feedback or just a value for an item
|
|
* @param stdClass $item
|
|
* @return array
|
|
*/
|
|
protected function get_values_tmp($item = null) {
|
|
global $DB;
|
|
if ($this->valuestmp === null) {
|
|
$completedtmp = $this->get_current_completed_tmp();
|
|
if ($completedtmp) {
|
|
$this->valuestmp = $DB->get_records_menu('feedback_valuetmp',
|
|
['completed' => $completedtmp->id], '', 'item, value');
|
|
} else {
|
|
$this->valuestmp = array();
|
|
}
|
|
}
|
|
if ($item) {
|
|
return array_key_exists($item->id, $this->valuestmp) ? $this->valuestmp[$item->id] : null;
|
|
}
|
|
return $this->valuestmp;
|
|
}
|
|
|
|
/**
|
|
* Returns all completed values for this feedback or just a value for an item
|
|
* @param stdClass $item
|
|
* @return array
|
|
*/
|
|
protected function get_values($item = null) {
|
|
global $DB;
|
|
if ($this->values === null) {
|
|
if ($this->completed) {
|
|
$this->values = $DB->get_records_menu('feedback_value',
|
|
['completed' => $this->completed->id], '', 'item, value');
|
|
} else {
|
|
$this->values = array();
|
|
}
|
|
}
|
|
if ($item) {
|
|
return array_key_exists($item->id, $this->values) ? $this->values[$item->id] : null;
|
|
}
|
|
return $this->values;
|
|
}
|
|
|
|
/**
|
|
* Splits the feedback items into pages
|
|
*
|
|
* Items that we definitely know at this stage as not applicable are excluded.
|
|
* Items that are dependent on something that has not yet been answered are
|
|
* still present, as well as items with broken dependencies.
|
|
*
|
|
* @return array array of arrays of items
|
|
*/
|
|
public function get_pages() {
|
|
$pages = [[]]; // The first page always exists.
|
|
$items = $this->get_items();
|
|
foreach ($items as $item) {
|
|
if ($item->typ === 'pagebreak') {
|
|
$pages[] = [];
|
|
} else if ($this->can_see_item($item) !== false) {
|
|
$pages[count($pages) - 1][] = $item;
|
|
}
|
|
}
|
|
return $pages;
|
|
}
|
|
|
|
/**
|
|
* Returns the last page that has items with the value (i.e. not label) which have been answered
|
|
* as well as the first page that has items with the values that have not been answered.
|
|
*
|
|
* Either of the two return values may be null if there are no answered page or there are no
|
|
* unanswered pages left respectively.
|
|
*
|
|
* Two pages may not be directly following each other because there may be empty pages
|
|
* or pages with information texts only between them
|
|
*
|
|
* @return array array of two elements [$lastcompleted, $firstincompleted]
|
|
*/
|
|
protected function get_last_completed_page() {
|
|
$completed = [];
|
|
$incompleted = [];
|
|
$pages = $this->get_pages();
|
|
foreach ($pages as $pageidx => $pageitems) {
|
|
foreach ($pageitems as $item) {
|
|
if ($item->hasvalue) {
|
|
if ($this->get_values_tmp($item) !== null) {
|
|
$completed[$pageidx] = true;
|
|
} else {
|
|
$incompleted[$pageidx] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$completed = array_keys($completed);
|
|
$incompleted = array_keys($incompleted);
|
|
// If some page has both completed and incompleted items it is considered incompleted.
|
|
$completed = array_diff($completed, $incompleted);
|
|
// If the completed page follows an incompleted page, it does not count.
|
|
$firstincompleted = $incompleted ? min($incompleted) : null;
|
|
if ($firstincompleted !== null) {
|
|
$completed = array_filter($completed, function($a) use ($firstincompleted) {
|
|
return $a < $firstincompleted;
|
|
});
|
|
}
|
|
$lastcompleted = $completed ? max($completed) : null;
|
|
return [$lastcompleted, $firstincompleted];
|
|
}
|
|
|
|
/**
|
|
* Get the next page for the feedback
|
|
*
|
|
* This is normally $gopage+1 but may be bigger if there are empty pages or
|
|
* pages without visible questions.
|
|
*
|
|
* This method can only be called when questions on the current page are
|
|
* already answered, otherwise it may be inaccurate.
|
|
*
|
|
* @param int $gopage current page
|
|
* @param bool $strictcheck when gopage is the user-input value, make sure we do not jump over unanswered questions
|
|
* @return int|null the index of the next page or null if this is the last page
|
|
*/
|
|
public function get_next_page($gopage, $strictcheck = true) {
|
|
if ($strictcheck) {
|
|
list($lastcompleted, $firstincompleted) = $this->get_last_completed_page();
|
|
if ($firstincompleted !== null && $firstincompleted <= $gopage) {
|
|
return $firstincompleted;
|
|
}
|
|
}
|
|
$pages = $this->get_pages();
|
|
for ($pageidx = $gopage + 1; $pageidx < count($pages); $pageidx++) {
|
|
if (!empty($pages[$pageidx])) {
|
|
return $pageidx;
|
|
}
|
|
}
|
|
// No further pages in the feedback have any visible items.
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the previous page for the feedback
|
|
*
|
|
* This is normally $gopage-1 but may be smaller if there are empty pages or
|
|
* pages without visible questions.
|
|
*
|
|
* @param int $gopage current page
|
|
* @param bool $strictcheck when gopage is the user-input value, make sure we do not jump over unanswered questions
|
|
* @return int|null the index of the next page or null if this is the first page with items
|
|
*/
|
|
public function get_previous_page($gopage, $strictcheck = true) {
|
|
if (!$gopage) {
|
|
// If we are already on the first (0) page, there is definitely no previous page.
|
|
return null;
|
|
}
|
|
$pages = $this->get_pages();
|
|
$rv = null;
|
|
// Iterate through previous pages and find the closest one that has any items on it.
|
|
for ($pageidx = $gopage - 1; $pageidx >= 0; $pageidx--) {
|
|
if (!empty($pages[$pageidx])) {
|
|
$rv = $pageidx;
|
|
break;
|
|
}
|
|
}
|
|
if ($rv === null) {
|
|
// We are on the very first page that has items.
|
|
return null;
|
|
}
|
|
if ($rv > 0 && $strictcheck) {
|
|
// Check if this page is actually not past than first incompleted page.
|
|
list($lastcompleted, $firstincompleted) = $this->get_last_completed_page();
|
|
if ($firstincompleted !== null && $firstincompleted < $rv) {
|
|
return $firstincompleted;
|
|
}
|
|
}
|
|
return $rv;
|
|
}
|
|
|
|
/**
|
|
* Page index to resume the feedback
|
|
*
|
|
* When user abandones answering feedback and then comes back to it we should send him
|
|
* to the first page after the last page he fully completed.
|
|
* @return int
|
|
*/
|
|
public function get_resume_page() {
|
|
list($lastcompleted, $firstincompleted) = $this->get_last_completed_page();
|
|
return $lastcompleted === null ? 0 : $this->get_next_page($lastcompleted, false);
|
|
}
|
|
|
|
/**
|
|
* Creates a new record in the 'feedback_completedtmp' table for the current user/guest session
|
|
*
|
|
* @return stdClass record from feedback_completedtmp or false if not found
|
|
*/
|
|
protected function create_current_completed_tmp() {
|
|
global $USER, $DB;
|
|
$record = (object)['feedback' => $this->feedback->id];
|
|
if ($this->get_courseid()) {
|
|
$record->courseid = $this->get_courseid();
|
|
}
|
|
if (isloggedin() && !isguestuser()) {
|
|
$record->userid = $USER->id;
|
|
} else {
|
|
$record->guestid = sesskey();
|
|
}
|
|
$record->timemodified = time();
|
|
$record->anonymous_response = $this->feedback->anonymous;
|
|
$id = $DB->insert_record('feedback_completedtmp', $record);
|
|
$this->completedtmp = $DB->get_record('feedback_completedtmp', ['id' => $id]);
|
|
$this->valuestmp = null;
|
|
return $this->completedtmp;
|
|
}
|
|
|
|
/**
|
|
* Saves unfinished response to the temporary table
|
|
*
|
|
* This is called when user proceeds to the next/previous page in the complete form
|
|
* and also right after the form submit.
|
|
* After the form submit the {@link save_response()} is called to
|
|
* move response from temporary table to completion table.
|
|
*
|
|
* @param stdClass $data data from the form mod_feedback_complete_form
|
|
*/
|
|
public function save_response_tmp($data) {
|
|
global $DB;
|
|
if (!$completedtmp = $this->get_current_completed_tmp()) {
|
|
$completedtmp = $this->create_current_completed_tmp();
|
|
} else {
|
|
$currentime = time();
|
|
$DB->update_record('feedback_completedtmp',
|
|
['id' => $completedtmp->id, 'timemodified' => $currentime]);
|
|
$completedtmp->timemodified = $currentime;
|
|
}
|
|
|
|
// Find all existing values.
|
|
$existingvalues = $DB->get_records_menu('feedback_valuetmp',
|
|
['completed' => $completedtmp->id], '', 'item, id');
|
|
|
|
// Loop through all feedback items and save the ones that are present in $data.
|
|
$allitems = $this->get_items();
|
|
foreach ($allitems as $item) {
|
|
if (!$item->hasvalue) {
|
|
continue;
|
|
}
|
|
$keyname = $item->typ . '_' . $item->id;
|
|
if (!isset($data->$keyname)) {
|
|
// This item is either on another page or dependency was not met - nothing to save.
|
|
continue;
|
|
}
|
|
|
|
$newvalue = ['item' => $item->id, 'completed' => $completedtmp->id, 'course_id' => $completedtmp->courseid];
|
|
|
|
// Convert the value to string that can be stored in 'feedback_valuetmp' or 'feedback_value'.
|
|
$itemobj = feedback_get_item_class($item->typ);
|
|
$newvalue['value'] = $itemobj->create_value($data->$keyname);
|
|
|
|
// Update or insert the value in the 'feedback_valuetmp' table.
|
|
if (array_key_exists($item->id, $existingvalues)) {
|
|
$newvalue['id'] = $existingvalues[$item->id];
|
|
$DB->update_record('feedback_valuetmp', $newvalue);
|
|
} else {
|
|
$DB->insert_record('feedback_valuetmp', $newvalue);
|
|
}
|
|
}
|
|
|
|
// Reset valuestmp cache.
|
|
$this->valuestmp = null;
|
|
}
|
|
|
|
/**
|
|
* Saves the response
|
|
*
|
|
* The form data has already been stored in the temporary table in
|
|
* {@link save_response_tmp()}. This function copies the values
|
|
* from the temporary table to the completion table.
|
|
* It is also responsible for sending email notifications when applicable.
|
|
*/
|
|
public function save_response() {
|
|
global $USER, $SESSION, $DB;
|
|
|
|
$feedbackcompleted = $this->find_last_completed();
|
|
$feedbackcompletedtmp = $this->get_current_completed_tmp();
|
|
|
|
if (feedback_check_is_switchrole()) {
|
|
// We do not actually save anything if the role is switched, just delete temporary values.
|
|
$this->delete_completedtmp();
|
|
return;
|
|
}
|
|
|
|
// Save values.
|
|
$completedid = feedback_save_tmp_values($feedbackcompletedtmp, $feedbackcompleted);
|
|
$this->completed = $DB->get_record('feedback_completed', array('id' => $completedid));
|
|
|
|
// Send email.
|
|
if ($this->feedback->anonymous == FEEDBACK_ANONYMOUS_NO) {
|
|
feedback_send_email($this->cm, $this->feedback, $this->cm->get_course(), $USER);
|
|
} else {
|
|
feedback_send_email_anonym($this->cm, $this->feedback, $this->cm->get_course());
|
|
}
|
|
|
|
unset($SESSION->feedback->is_started);
|
|
|
|
// Update completion state.
|
|
$completion = new completion_info($this->cm->get_course());
|
|
if (isloggedin() && !isguestuser() && $completion->is_enabled($this->cm) && $this->feedback->completionsubmit) {
|
|
$completion->update_state($this->cm, COMPLETION_COMPLETE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes the temporary completed and all related temporary values
|
|
*/
|
|
protected function delete_completedtmp() {
|
|
global $DB;
|
|
|
|
if ($completedtmp = $this->get_current_completed_tmp()) {
|
|
$DB->delete_records('feedback_valuetmp', ['completed' => $completedtmp->id]);
|
|
$DB->delete_records('feedback_completedtmp', ['id' => $completedtmp->id]);
|
|
$this->completedtmp = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves the last completion record for the current user
|
|
*
|
|
* @return stdClass record from feedback_completed or false if not found
|
|
*/
|
|
protected function find_last_completed() {
|
|
global $USER, $DB;
|
|
if (isloggedin() || isguestuser()) {
|
|
// Not possible to retrieve completed feedback for guests.
|
|
return false;
|
|
}
|
|
if ($this->is_anonymous()) {
|
|
// Not possible to retrieve completed anonymous feedback.
|
|
return false;
|
|
}
|
|
$params = array('feedback' => $this->feedback->id, 'userid' => $USER->id);
|
|
if ($this->get_courseid()) {
|
|
$params['courseid'] = $this->get_courseid();
|
|
}
|
|
$this->completed = $DB->get_record('feedback_completed', $params);
|
|
return $this->completed;
|
|
}
|
|
|
|
/**
|
|
* Checks if current user has capability to submit the feedback
|
|
*
|
|
* There is an exception for fully anonymous feedbacks when guests can complete
|
|
* feedback without the proper capability.
|
|
*
|
|
* This should be followed by checking {@link can_submit()} because even if
|
|
* user has capablity to complete, they may have already submitted feedback
|
|
* and can not re-submit
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function can_complete() {
|
|
global $CFG;
|
|
|
|
$context = context_module::instance($this->cm->id);
|
|
if (has_capability('mod/feedback:complete', $context)) {
|
|
return true;
|
|
}
|
|
|
|
if (!empty($CFG->feedback_allowfullanonymous)
|
|
AND $this->feedback->course == SITEID
|
|
AND $this->feedback->anonymous == FEEDBACK_ANONYMOUS_YES
|
|
AND (!isloggedin() OR isguestuser())) {
|
|
// Guests are allowed to complete fully anonymous feedback without having 'mod/feedback:complete' capability.
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks if user is prevented from re-submission.
|
|
*
|
|
* This must be called after {@link can_complete()}
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function can_submit() {
|
|
if ($this->get_feedback()->multiple_submit == 0 ) {
|
|
if ($this->is_already_submitted()) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
} |