Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?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;/** @var mod_feedback_complete_form the form used for completing the feedback */protected $form = null;/** @var bool true when the feedback has been completed during the request */protected $justcompleted = false;/** @var int the next page the user should jump after processing the form */protected $jumpto = null;/*** Constructor** @param stdClass $feedback feedback object* @param cm_info $cm course module object corresponding to the $feedback* (at least one of $feedback or $cm is required)* @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 $nonanonymouseuserid - Return only anonymous results or specified user's results.* If null only anonymous replies will be returned and the $completedid is mandatory.* If specified only non-anonymous replies of $nonanonymouseuserid will be returned.* @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default).*/public function __construct($feedback, $cm, $courseid, $iscompleted = false, $completedid = null,$nonanonymouseuserid = null, $userid = 0) {global $DB;parent::__construct($feedback, $cm, $courseid, 0, $userid);// Make sure courseid is always set for site feedback.if ($this->feedback->course == SITEID && !$this->courseid) {$this->courseid = SITEID;}if ($iscompleted) {// Retrieve information about the completion.$this->iscompleted = true;$params = array('feedback' => $this->feedback->id);if (!$nonanonymouseuserid && !$completedid) {throw new coding_exception('Either $completedid or $nonanonymouseuserid must be specified for completed feedbacks');}if ($completedid) {$params['id'] = $completedid;}if ($nonanonymouseuserid) {// 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'] = $nonanonymouseuserid;}$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;}/*** Check if the feedback was just completed.** @return bool true if the feedback was just completed.* @since Moodle 3.3*/public function just_completed() {return $this->justcompleted;}/*** Return the jumpto property.** @return int the next page to jump.* @since Moodle 3.3*/public function get_jumpto() {return $this->jumpto;}/*** Returns the temporary completion record for the current user or guest session** @return stdClass|false record from feedback_completedtmp or false if not found*/public function get_current_completed_tmp() {global $DB, $USER;if ($this->completedtmp === null) {$params = array('feedback' => $this->get_feedback()->id);if ($courseid = $this->get_courseid()) {$params['courseid'] = $courseid;}if ((isloggedin() || $USER->id != $this->userid) && !isguestuser($this->userid)) {$params['userid'] = $this->userid;} 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,* true if there is no dependency or dependency is met,* false if dependent question is visible or broken* and further it is either not answered or the dependency is not met,* null if dependency is broken.*/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) {// Cyclic dependencies are no problem here, since they will throw an dependency error above.if ($this->can_see_item($ditem) === false) {return false;}return null;}$check = $itemobj->compare_value($ditem, $value, $item->dependvalue) ? true : false;if ($check) {return $this->can_see_item($ditem);}return 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);}}/*** Retrieves responses from an unfinished attempt.** @return array the responses (from the feedback_valuetmp table)* @since Moodle 3.3*/public function get_unfinished_responses() {global $DB;$responses = array();$completedtmp = $this->get_current_completed_tmp();if ($completedtmp) {$responses = $DB->get_records('feedback_valuetmp', ['completed' => $completedtmp->id]);}return $responses;}/*** 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) {$this->valuestmp = array();$responses = $this->get_unfinished_responses();foreach ($responses as $r) {$this->valuestmp[$r->item] = $r->value;}}if ($item) {return array_key_exists($item->id, $this->valuestmp) ? $this->valuestmp[$item->id] : null;}return $this->valuestmp;}/*** Retrieves responses from an finished attempt.** @return array the responses (from the feedback_value table)* @since Moodle 3.3*/public function get_finished_responses() {global $DB;$responses = array();if ($this->completed) {$responses = $DB->get_records('feedback_value', ['completed' => $this->completed->id]);}return $responses;}/*** 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) {$this->values = array();$responses = $this->get_finished_responses();foreach ($responses as $r) {$this->values[$r->item] = $r->value;}}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 $DB, $USER;$record = (object)['feedback' => $this->feedback->id];if ($this->get_courseid()) {$record->courseid = $this->get_courseid();}if ((isloggedin() || $USER->id != $this->userid) && !isguestuser($this->userid)) {$record->userid = $this->userid;} 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;}/*** If user has already completed the feedback, create the temproray values from last completed attempt** @return stdClass record from feedback_completedtmp or false if not found*/public function create_completed_tmp_from_last_completed() {if (!$this->get_current_completed_tmp()) {$lastcompleted = $this->find_last_completed();if ($lastcompleted) {$this->completedtmp = feedback_set_tmp_values($lastcompleted);}}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 $SESSION, $DB, $USER;$feedbackcompleted = $this->find_last_completed();// If no record is found, change false to null for safe use in feedback_save_tmp_values.$feedbackcompleted = !$feedbackcompleted ? null : $feedbackcompleted;$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(), $this->userid, $this->completed);} 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() || $USER->id != $this->userid) && $completion->is_enabled($this->cm) &&$this->cm->completion == COMPLETION_TRACKING_AUTOMATIC && $this->feedback->completionsubmit) {$completion->update_state($this->cm, COMPLETION_COMPLETE, $this->userid);}}/*** 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*/public function find_last_completed() {global $DB, $USER;if ((!isloggedin() && $USER->id == $this->userid) || isguestuser($this->userid)) {// 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' => $this->userid,'anonymous_response' => FEEDBACK_ANONYMOUS_NO);if ($this->get_courseid()) {$params['courseid'] = $this->get_courseid();}$this->completed = $DB->get_record('feedback_completed', $params);return $this->completed;}/*** Checks if 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, $USER;$context = context_module::instance($this->cm->id);if (has_capability('mod/feedback:complete', $context, $this->userid)) {return true;}if (!empty($CFG->feedback_allowfullanonymous)AND $this->feedback->course == SITEIDAND $this->feedback->anonymous == FEEDBACK_ANONYMOUS_YESAND ((!isloggedin() && $USER->id == $this->userid) || isguestuser($this->userid))) {// 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;}/*** Trigger module viewed event.** @since Moodle 3.3*/public function trigger_module_viewed() {$event = \mod_feedback\event\course_module_viewed::create_from_record($this->feedback, $this->cm, $this->cm->get_course());$event->trigger();}/*** Mark activity viewed for completion-tracking.** @since Moodle 3.3*/public function set_module_viewed() {global $CFG;require_once($CFG->libdir . '/completionlib.php');$completion = new completion_info($this->cm->get_course());$completion->set_module_viewed($this->cm, $this->userid);}/*** Process a page jump via the mod_feedback_complete_form.** This function initializes the form and process the submission.** @param int $gopage the current page* @param int $gopreviouspage if the user chose to go to the previous page* @return string the url to redirect the user (if any)* @since Moodle 3.3*/public function process_page($gopage, $gopreviouspage = false) {global $CFG, $PAGE, $SESSION;$urltogo = null;// Save the form for later during the request.$this->create_completed_tmp_from_last_completed();$this->form = new mod_feedback_complete_form(mod_feedback_complete_form::MODE_COMPLETE,$this, 'feedback_complete_form', array('gopage' => $gopage));if ($this->form->is_cancelled()) {// Form was cancelled - return to the course page.$urltogo = new moodle_url('/mod/feedback/view.php', ['id' => $this->get_cm()->id]);} else if ($this->form->is_submitted() &&($this->form->is_validated() || $gopreviouspage)) {// Form was submitted (skip validation for "Previous page" button).$data = $this->form->get_submitted_data();if (!isset($SESSION->feedback->is_started) OR !$SESSION->feedback->is_started == true) {throw new \moodle_exception('error', '', $CFG->wwwroot.'/course/view.php?id='.$this->courseid);}$this->save_response_tmp($data);if (!empty($data->savevalues) || !empty($data->gonextpage)) {if (($nextpage = $this->get_next_page($gopage)) !== null) {if ($PAGE->has_set_url()) {$urltogo = new moodle_url($PAGE->url, array('gopage' => $nextpage));}$this->jumpto = $nextpage;} else {$this->save_response();if (!$this->get_feedback()->page_after_submit) {\core\notification::success(get_string('entries_saved', 'feedback'));}$this->justcompleted = true;}} else if (!empty($gopreviouspage)) {$prevpage = intval($this->get_previous_page($gopage));if ($PAGE->has_set_url()) {$urltogo = new moodle_url($PAGE->url, array('gopage' => $prevpage));}$this->jumpto = $prevpage;}}return $urltogo;}/*** Render the form with the questions.** @return string the form rendered* @since Moodle 3.3*/public function render_items() {global $SESSION;// Print the items.$SESSION->feedback->is_started = true;return $this->form->render();}}