AutorÃa | 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/>.use mod_questionnaire\feedback\section;defined('MOODLE_INTERNAL') || die();require_once($CFG->dirroot.'/mod/questionnaire/locallib.php');/*** Provided the main API functions for questionnaire.** @package mod_questionnaire* @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org)* @author Mike Churchward* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class questionnaire {// Class Properties./*** @var \mod_questionnaire\question\question[] $quesitons*/public $questions = [];/*** The survey record.* @var object $survey*/// Todo var $survey; TODO./*** @var $renderer Contains the page renderer when loaded, or false if not.*/public $renderer = false;/*** @var $page Contains the renderable, templatable page when loaded, or false if not.*/public $page = false;// Class Methods./*** The constructor.* @param stdClass $course* @param stdClass $cm* @param int $id* @param null|stdClass $questionnaire* @param bool $addquestions* @throws dml_exception*/public function __construct(&$course, &$cm, $id = 0, $questionnaire = null, $addquestions = true) {global $DB;if ($id) {$questionnaire = $DB->get_record('questionnaire', array('id' => $id));}if (is_object($questionnaire)) {$properties = get_object_vars($questionnaire);foreach ($properties as $property => $value) {$this->$property = $value;}}if (!empty($this->sid)) {$this->add_survey($this->sid);}$this->course = $course;$this->cm = $cm;// When we are creating a brand new questionnaire, we will not yet have a context.if (!empty($cm) && !empty($this->id)) {$this->context = context_module::instance($cm->id);} else {$this->context = null;}if ($addquestions && !empty($this->sid)) {$this->add_questions($this->sid);}// Load the capabilities for this user and questionnaire, if not creating a new one.if (!empty($this->cm->id)) {$this->capabilities = questionnaire_load_capabilities($this->cm->id);}// Don't automatically add responses.$this->responses = [];}/*** Adding a survey record to the object.* @param int $sid* @param null $survey*/public function add_survey($sid = 0, $survey = null) {global $DB;if ($sid) {$this->survey = $DB->get_record('questionnaire_survey', array('id' => $sid));} else if (is_object($survey)) {$this->survey = clone($survey);}}/*** Adding questions to the object.* @param bool $sid*/public function add_questions($sid = false) {global $DB;if ($sid === false) {$sid = $this->sid;}if (!isset($this->questions)) {$this->questions = [];$this->questionsbysec = [];}$select = 'surveyid = ? AND deleted = ?';$params = [$sid, 'n'];if ($records = $DB->get_records_select('questionnaire_question', $select, $params, 'position')) {$sec = 1;$isbreak = false;foreach ($records as $record) {$this->questions[$record->id] = \mod_questionnaire\question\question::question_builder($record->type_id,$record, $this->context);if ($record->type_id != QUESPAGEBREAK) {$this->questionsbysec[$sec][] = $record->id;$isbreak = false;} else {// Sanity check: no section break allowed as first position, no 2 consecutive section breaks.if ($record->position != 1 && $isbreak == false) {$sec++;$isbreak = true;}}}}}/*** Load all response information for this user.** @param int $userid*/public function add_user_responses($userid = null) {global $USER, $DB;// Empty questionnaires cannot have responses.if (empty($this->id)) {return;}if ($userid === null) {$userid = $USER->id;}$responses = $this->get_responses($userid);foreach ($responses as $response) {$this->responses[$response->id] = mod_questionnaire\responsetype\response\response::create_from_data($response);}}/*** Load the specified response information.** @param int $responseid*/public function add_response(int $responseid) {global $DB;// Empty questionnaires cannot have responses.if (empty($this->id)) {return;}$response = $DB->get_record('questionnaire_response', ['id' => $responseid]);$this->responses[$response->id] = mod_questionnaire\responsetype\response\response::create_from_data($response);}/*** Load the response information from a submitted web form.** @param stdClass $formdata*/public function add_response_from_formdata(stdClass $formdata) {$this->responses[0] = mod_questionnaire\responsetype\response\response::response_from_webform($formdata, $this->questions);}/*** Return a response object from a submitted mobile app form.** @param stdClass $appdata* @param int $sec* @return bool|\mod_questionnaire\responsetype\response\response*/public function build_response_from_appdata(stdClass $appdata, $sec=0) {$questions = [];if ($sec == 0) {$questions = $this->questions;} else {foreach ($this->questionsbysec[$sec] as $questionid) {$questions[$questionid] = $this->questions[$questionid];}}return mod_questionnaire\responsetype\response\response::response_from_appdata($this->id, 0, $appdata, $questions);}/*** Add the renderer to the questionnaire object.* @param plugin_renderer_base $renderer The module renderer, extended from core renderer.*/public function add_renderer(plugin_renderer_base $renderer) {$this->renderer = $renderer;}/*** Add the templatable page to the questionnaire object.* @param templatable $page The page to render, implementing core classes.*/public function add_page($page) {$this->page = $page;}/*** Return true if questions should be automatically numbered.* @return bool*/public function questions_autonumbered() {// Value of 1 if questions should be numbered. Value of 3 if both questions and pages should be numbered.return (!empty($this->autonum) && (($this->autonum == 1) || ($this->autonum == 3)));}/*** Return true if pages should be automatically numbered.* @return bool*/public function pages_autonumbered() {// Value of 2 if pages should be numbered. Value of 3 if both questions and pages should be numbered.return (!empty($this->autonum) && (($this->autonum == 2) || ($this->autonum == 3)));}/*** The main module view function.*/public function view() {global $CFG, $USER, $PAGE;$PAGE->set_title(format_string($this->name));$PAGE->set_heading(format_string($this->course->fullname));// Initialise the JavaScript.$PAGE->requires->js_init_call('M.mod_questionnaire.init_attempt_form', null, false, questionnaire_get_js_module());$message = $this->user_access_messages($USER->id, true);if ($message !== false) {$this->page->add_to_page('notifications', $message);} else {// Handle the main questionnaire completion page.$quser = $USER->id;$msg = $this->print_survey($quser, $USER->id);// If Questionnaire was submitted with all required fields completed ($msg is empty),// then record the submittal.$viewform = data_submitted($CFG->wwwroot."/mod/questionnaire/complete.php");if ($viewform && confirm_sesskey() && isset($viewform->submit) && isset($viewform->submittype) &&($viewform->submittype == "Submit Survey") && empty($msg)) {if (!empty($viewform->rid)) {$viewform->rid = (int)$viewform->rid;}if (!empty($viewform->sec)) {$viewform->sec = (int)$viewform->sec;}$this->response_delete($viewform->rid, $viewform->sec);$this->rid = $this->response_insert($viewform, $quser);$this->response_commit($this->rid);$this->update_grades($quser);// Update completion state.$completion = new completion_info($this->course);if ($completion->is_enabled($this->cm) && $this->completionsubmit) {$completion->update_state($this->cm, COMPLETION_COMPLETE);}// Log this submitted response. Note this removes the anonymity in the logged event.$context = context_module::instance($this->cm->id);$anonymous = $this->respondenttype == 'anonymous';$params = array('context' => $context,'courseid' => $this->course->id,'relateduserid' => $USER->id,'anonymous' => $anonymous,'other' => array('questionnaireid' => $this->id));$event = \mod_questionnaire\event\attempt_submitted::create($params);$event->trigger();$this->submission_notify($this->rid);$this->response_goto_thankyou();}}}/*** Delete the specified response, and insert a new one.* @param int $rid* @param int $sec* @param int $quser* @return bool|int*/public function delete_insert_response($rid, $sec, $quser) {$this->response_delete($rid, $sec);$this->rid = $this->response_insert((object)['sec' => $sec, 'rid' => $rid], $quser);return $this->rid;}/*** Commit the response.* @param int $rid* @param int $quser*/public function commit_submission_response($rid, $quser) {$this->response_commit($rid);// If it was a previous save, rid is in the form...if (!empty($rid) && is_numeric($rid)) {$rid = $rid;// Otherwise its in this object.} else {$rid = $this->rid;}$this->update_grades($quser);// Update completion state.$completion = new \completion_info($this->course);if ($completion->is_enabled($this->cm) && $this->completionsubmit) {$completion->update_state($this->cm, COMPLETION_COMPLETE);}// Log this submitted response.$context = \context_module::instance($this->cm->id);$anonymous = $this->respondenttype == 'anonymous';$params = ['context' => $context,'courseid' => $this->course->id,'relateduserid' => $quser,'anonymous' => $anonymous,'other' => array('questionnaireid' => $this->id)];$event = \mod_questionnaire\event\attempt_submitted::create($params);$event->trigger();}/*** Update the grade for this questionnaire and user.** @param int $userid*/private function update_grades($userid) {if ($this->grade != 0) {$questionnaire = new \stdClass();$questionnaire->id = $this->id;$questionnaire->name = $this->name;$questionnaire->grade = $this->grade;$questionnaire->cmidnumber = $this->cm->idnumber;$questionnaire->courseid = $this->course->id;questionnaire_update_grades($questionnaire, $userid);}}/*** Function to view an entire responses data.* @param int $rid* @param string $referer* @param string $resps* @param bool $compare* @param bool $isgroupmember* @param bool $allresponses* @param int $currentgroupid* @param string $outputtarget*/public function view_response($rid, $referer= '', $resps = '', $compare = false, $isgroupmember = false, $allresponses = false,$currentgroupid = 0, $outputtarget = 'html') {$this->print_survey_start('', 1, 1, 0, $rid, false, $outputtarget);$i = 0;$this->add_response($rid);if ($referer != 'print') {$feedbackmessages = $this->response_analysis($rid, $resps, $compare, $isgroupmember, $allresponses, $currentgroupid);if ($feedbackmessages) {$msgout = '';foreach ($feedbackmessages as $msg) {$msgout .= $msg;}$this->page->add_to_page('feedbackmessages', $msgout);}if ($this->survey->feedbacknotes) {$text = file_rewrite_pluginfile_urls($this->survey->feedbacknotes, 'pluginfile.php',$this->context->id, 'mod_questionnaire', 'feedbacknotes', $this->survey->id);$this->page->add_to_page('feedbacknotes', $this->renderer->box(format_text($text, FORMAT_HTML)));}}$pdf = ($outputtarget == 'pdf') ? true : false;foreach ($this->questions as $question) {if ($question->type_id < QUESPAGEBREAK) {$i++;}if ($question->type_id != QUESPAGEBREAK) {$this->page->add_to_page('responses',$this->renderer->response_output($question, $this->responses[$rid], $i, $pdf));}}}/*** Function to view all loaded responses.*/public function view_all_responses() {$this->print_survey_start('', 1, 1, 0);// If a student's responses have been deleted by teacher while student was viewing the report,// then responses may have become empty, hence this test is necessary.if (!empty($this->responses)) {$this->page->add_to_page('responses', $this->renderer->all_response_output($this->responses, $this->questions));} else {$this->page->add_to_page('responses', $this->renderer->all_response_output(get_string('noresponses', 'questionnaire')));}$this->print_survey_end(1, 1);}// Access Methods./*** True if the questionnaire is active.* @return bool*/public function is_active() {return (!empty($this->survey));}/*** True if the questionnaire is open.* @return bool*/public function is_open() {return ($this->opendate > 0) ? ($this->opendate < time()) : true;}/*** True if the questionnaire is closed.* @return bool*/public function is_closed() {return ($this->closedate > 0) ? ($this->closedate < time()) : false;}/*** True if the specified user can complete this questionnaire.* @param int $userid* @return bool*/public function user_can_take($userid) {if (!$this->is_active() || !$this->user_is_eligible($userid)) {return false;} else if ($this->qtype == QUESTIONNAIREUNLIMITED) {return true;} else if ($userid > 0) {return $this->user_time_for_new_attempt($userid);} else {return false;}}/*** True if the specified user is eligible to complete this questionnaire.* @param int $userid* @return bool*/public function user_is_eligible($userid) {return ($this->capabilities->view && $this->capabilities->submit);}/*** Return any message if the user cannot complete this questionnaire, explaining why.* @param int $userid* @param bool $asnotification Return as a rendered notification.* @return bool|string*/public function user_access_messages($userid = 0, $asnotification = false) {global $USER;if ($userid == 0) {$userid = $USER->id;}$message = false;if (!$this->is_active()) {if ($this->capabilities->manage) {$msg = 'removenotinuse';} else {$msg = 'notavail';}$message = get_string($msg, 'questionnaire');} else if ($this->survey->realm == 'template') {$message = get_string('templatenotviewable', 'questionnaire');} else if (!$this->is_open()) {$message = get_string('notopen', 'questionnaire', userdate($this->opendate));} else if ($this->is_closed()) {$message = get_string('closed', 'questionnaire', userdate($this->closedate));} else if (!$this->user_is_eligible($userid)) {$message = get_string('noteligible', 'questionnaire');} else if (!$this->user_can_take($userid)) {switch ($this->qtype) {case QUESTIONNAIREDAILY:$msgstring = ' ' . get_string('today', 'questionnaire');break;case QUESTIONNAIREWEEKLY:$msgstring = ' ' . get_string('thisweek', 'questionnaire');break;case QUESTIONNAIREMONTHLY:$msgstring = ' ' . get_string('thismonth', 'questionnaire');break;default:$msgstring = '';break;}$message = get_string("alreadyfilled", "questionnaire", $msgstring);}if (($message !== false) && $asnotification) {$message = $this->renderer->notification($message, \core\output\notification::NOTIFY_ERROR);}return $message;}/*** True if the specified user has a saved response for this questionnaire.* @param int $userid* @return bool*/public function user_has_saved_response($userid) {global $DB;return $DB->record_exists('questionnaire_response',['questionnaireid' => $this->id, 'userid' => $userid, 'complete' => 'n']);}/*** True if the specified user can complete this questionnaire at this time.* @param int $userid* @return bool*/public function user_time_for_new_attempt($userid) {global $DB;$params = ['questionnaireid' => $this->id, 'userid' => $userid, 'complete' => 'y'];if (!($attempts = $DB->get_records('questionnaire_response', $params, 'submitted DESC'))) {return true;}$attempt = reset($attempts);$timenow = time();switch ($this->qtype) {case QUESTIONNAIREUNLIMITED:$cantake = true;break;case QUESTIONNAIREONCE:$cantake = false;break;case QUESTIONNAIREDAILY:$attemptyear = date('Y', $attempt->submitted);$currentyear = date('Y', $timenow);$attemptdayofyear = date('z', $attempt->submitted);$currentdayofyear = date('z', $timenow);$cantake = (($attemptyear < $currentyear) ||(($attemptyear == $currentyear) && ($attemptdayofyear < $currentdayofyear)));break;case QUESTIONNAIREWEEKLY:$attemptyear = date('Y', $attempt->submitted);$currentyear = date('Y', $timenow);$attemptweekofyear = date('W', $attempt->submitted);$currentweekofyear = date('W', $timenow);$cantake = (($attemptyear < $currentyear) ||(($attemptyear == $currentyear) && ($attemptweekofyear < $currentweekofyear)));break;case QUESTIONNAIREMONTHLY:$attemptyear = date('Y', $attempt->submitted);$currentyear = date('Y', $timenow);$attemptmonthofyear = date('n', $attempt->submitted);$currentmonthofyear = date('n', $timenow);$cantake = (($attemptyear < $currentyear) ||(($attemptyear == $currentyear) && ($attemptmonthofyear < $currentmonthofyear)));break;default:$cantake = false;break;}return $cantake;}/*** True if the accessing course contains the actual questionnaire, as opposed to an instance of a public questionnaire.* @return bool*/public function is_survey_owner() {return (!empty($this->survey->courseid) && ($this->course->id == $this->survey->courseid));}/*** True if the user can view the specified response.* @param int $rid* @return bool|void*/public function can_view_response($rid) {global $USER, $DB;if (!empty($rid)) {$response = $DB->get_record('questionnaire_response', array('id' => $rid));// If the response was not found, can't view it.if (empty($response)) {return false;}// If the response belongs to a different survey than this one, can't view it.if ($response->questionnaireid != $this->id) {return false;}// If you can view all responses always, then you can view it.if ($this->capabilities->readallresponseanytime) {return true;}// If you are allowed to view this response for another user.// If resp_view is set to QUESTIONNAIRE_STUDENTVIEWRESPONSES_NEVER, then this will always be false.if ($this->capabilities->readallresponses &&($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_ALWAYS ||($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENCLOSED && $this->is_closed()) ||($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENANSWERED && !$this->user_can_take($USER->id)))) {return true;}// If you can read your own response.if (($response->userid == $USER->id) && $this->capabilities->readownresponses &&($this->count_submissions($USER->id) > 0)) {return true;}} else {// If you can view all responses always, then you can view it.if ($this->capabilities->readallresponseanytime) {return true;}// If you are allowed to view this response for another user.// If resp_view is set to QUESTIONNAIRE_STUDENTVIEWRESPONSES_NEVER, then this will always be false.if ($this->capabilities->readallresponses &&($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_ALWAYS ||($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENCLOSED && $this->is_closed()) ||($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENANSWERED && !$this->user_can_take($USER->id)))) {return true;}// If you can read your own response.if ($this->capabilities->readownresponses && ($this->count_submissions($USER->id) > 0)) {return true;}}}/*** True if the user can view the responses to this questionnaire, and there are valid responses.* @param null|int $usernumresp* @return bool*/public function can_view_all_responses($usernumresp = null) {global $USER, $SESSION;$owner = $this->is_survey_owner();$numresp = $this->count_submissions();if ($usernumresp === null) {$usernumresp = $this->count_submissions($USER->id);}// Number of Responses in currently selected group (or all participants etc.).if (isset($SESSION->questionnaire->numselectedresps)) {$numselectedresps = $SESSION->questionnaire->numselectedresps;} else {$numselectedresps = $numresp;}// If questionnaire is set to separate groups, prevent user who is not member of any group// to view All responses.$canviewgroups = true;$canviewallgroups = has_capability('moodle/site:accessallgroups', $this->context);$groupmode = groups_get_activity_groupmode($this->cm, $this->course);if ($groupmode == 1) {$canviewgroups = groups_has_membership($this->cm, $USER->id);}$grouplogic = $canviewgroups || $canviewallgroups;$respslogic = ($numresp > 0) && ($numselectedresps > 0);return $this->can_view_all_responses_anytime($grouplogic, $respslogic) ||$this->can_view_all_responses_with_restrictions($usernumresp, $grouplogic, $respslogic);}/*** True if the user can view all of the responses to this questionnaire any time, and there are valid responses.* @param bool $grouplogic* @param bool $respslogic* @return bool*/public function can_view_all_responses_anytime($grouplogic = true, $respslogic = true) {// Can view if you are a valid group user, this is the owning course, and there are responses, and you have no// response view restrictions.return $grouplogic && $respslogic && $this->is_survey_owner() && $this->capabilities->readallresponseanytime;}/*** True if the user can view all of the responses to this questionnaire any time, and there are valid responses.* @param null|int $usernumresp* @param bool $grouplogic* @param bool $respslogic* @return bool*/public function can_view_all_responses_with_restrictions($usernumresp, $grouplogic = true, $respslogic = true) {// Can view if you are a valid group user, this is the owning course, and there are responses, and you can view// subject to viewing settings..return $grouplogic && $respslogic && $this->is_survey_owner() &&($this->capabilities->readallresponses &&($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_ALWAYS ||($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENCLOSED && $this->is_closed()) ||($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENANSWERED && $usernumresp)));}/*** Return the number of submissions for this questionnaire.* @param bool $userid* @param int $groupid* @return int*/public function count_submissions($userid=false, $groupid=0) {global $DB;$params = [];$groupsql = '';$groupcnd = '';if ($groupid != 0) {$groupsql = 'INNER JOIN {groups_members} gm ON r.userid = gm.userid ';$groupcnd = ' AND gm.groupid = :groupid ';$params['groupid'] = $groupid;}// Since submission can be across questionnaires in the case of public questionnaires, need to check the realm.// Public questionnaires can have responses to multiple questionnaire instances.if ($this->survey_is_public_master()) {$sql = 'SELECT COUNT(r.id) ' .'FROM {questionnaire_response} r ' .'INNER JOIN {questionnaire} q ON r.questionnaireid = q.id ' .'INNER JOIN {questionnaire_survey} s ON q.sid = s.id ' .$groupsql .'WHERE s.id = :surveyid AND r.complete = :status' . $groupcnd;$params['surveyid'] = $this->sid;$params['status'] = 'y';} else {$sql = 'SELECT COUNT(r.id) ' .'FROM {questionnaire_response} r ' .$groupsql .'WHERE r.questionnaireid = :questionnaireid AND r.complete = :status' . $groupcnd;$params['questionnaireid'] = $this->id;$params['status'] = 'y';}if ($userid) {$sql .= ' AND r.userid = :userid';$params['userid'] = $userid;}return $DB->count_records_sql($sql, $params);}/*** Get the requested responses for this questionnaire.** @param int|bool $userid* @param int $groupid* @return array*/public function get_responses($userid=false, $groupid=0) {global $DB;$params = [];$groupsql = '';$groupcnd = '';if ($groupid != 0) {$groupsql = 'INNER JOIN {groups_members} gm ON r.userid = gm.userid ';$groupcnd = ' AND gm.groupid = :groupid ';$params['groupid'] = $groupid;}// Since submission can be across questionnaires in the case of public questionnaires, need to check the realm.// Public questionnaires can have responses to multiple questionnaire instances.if ($this->survey_is_public_master()) {$sql = 'SELECT r.* ' .'FROM {questionnaire_response} r ' .'INNER JOIN {questionnaire} q ON r.questionnaireid = q.id ' .'INNER JOIN {questionnaire_survey} s ON q.sid = s.id ' .$groupsql .'WHERE s.id = :surveyid AND r.complete = :status' . $groupcnd;$params['surveyid'] = $this->sid;$params['status'] = 'y';} else {$sql = 'SELECT r.* ' .'FROM {questionnaire_response} r ' .$groupsql .'WHERE r.questionnaireid = :questionnaireid AND r.complete = :status' . $groupcnd;$params['questionnaireid'] = $this->id;$params['status'] = 'y';}if ($userid) {$sql .= ' AND r.userid = :userid';$params['userid'] = $userid;}$sql .= ' ORDER BY r.id';return $DB->get_records_sql($sql, $params) ?? [];}/*** True if any of the questions are required.* @param int $section* @return bool*/private function has_required($section = 0) {if (empty($this->questions)) {return false;} else if ($section <= 0) {foreach ($this->questions as $question) {if ($question->required()) {return true;}}} else {foreach ($this->questionsbysec[$section] as $questionid) {if ($this->questions[$questionid]->required()) {return true;}}}return false;}/*** Check if current questionnaire has dependencies set and any question has dependencies.** @return boolean Whether dependencies are set or not.*/public function has_dependencies() {$hasdependencies = false;if (($this->navigate > 0) && isset($this->questions) && !empty($this->questions)) {foreach ($this->questions as $question) {if ($question->has_dependencies()) {$hasdependencies = true;break;}}}return $hasdependencies;}/*** Get a list of all dependent questions.* @param int $questionid* @return array*/public function get_all_dependants($questionid) {$directids = $this->get_dependants($questionid);$directs = [];$indirects = [];foreach ($directids as $directid) {$this->load_parents($this->questions[$directid]);$indirectids = $this->get_dependants($directid);foreach ($this->questions[$directid]->dependencies as $dep) {if ($dep->dependquestionid == $questionid) {$directs[$directid][] = $dep;}}foreach ($indirectids as $indirectid) {$this->load_parents($this->questions[$indirectid]);foreach ($this->questions[$indirectid]->dependencies as $dep) {if ($dep->dependquestionid != $questionid) {$indirects[$indirectid][] = $dep;}}}}$alldependants = new stdClass();$alldependants->directs = $directs;$alldependants->indirects = $indirects;return($alldependants);}/*** Get a list of all dependent questions.* @param int $questionid* @return array*/public function get_dependants($questionid) {$qu = [];// Create an array which shows for every question the child-IDs.foreach ($this->questions as $question) {if ($question->has_dependencies()) {foreach ($question->dependencies as $dependency) {if (($dependency->dependquestionid == $questionid) && !in_array($question->id, $qu)) {$qu[] = $question->id;}}}}return($qu);}/*** Function to sort descendants array in get_dependants function.* @param mixed $a* @param mixed $b* @return int*/private static function cmp($a, $b) {if ($a == $b) {return 0;} else if ($a < $b) {return -1;} else {return 1;}}/*** Get all descendants and choices for questions with descendants.* @return array*/public function get_dependants_and_choices() {$questions = array_reverse($this->questions, true);$parents = [];foreach ($questions as $question) {foreach ($question->dependencies as $dependency) {$child = new stdClass();$child->choiceid = $dependency->dependchoiceid;$child->logic = $dependency->dependlogic;$child->andor = $dependency->dependandor;$parents[$dependency->dependquestionid][$question->id][] = $child;}}return($parents);}/*** Load needed parent question information into the dependencies structure for the requested question.* @param \mod_questionnaire\question\question $question* @return bool*/public function load_parents($question) {foreach ($question->dependencies as $did => $dependency) {$dependquestion = $this->questions[$dependency->dependquestionid];$qdependchoice = '';switch ($dependquestion->type_id) {case QUESRADIO:case QUESDROP:case QUESCHECK:$qdependchoice = $dependency->dependchoiceid;$dependchoice = $dependquestion->choices[$dependency->dependchoiceid]->content;$contents = questionnaire_choice_values($dependchoice);if ($contents->modname) {$dependchoice = $contents->modname;}break;case QUESYESNO:switch ($dependency->dependchoiceid) {case 0:$dependchoice = get_string('yes');$qdependchoice = 'y';break;case 1:$dependchoice = get_string('no');$qdependchoice = 'n';break;}break;}// Qdependquestion, parenttype and qdependchoice fields to be used in preview mode.$question->dependencies[$did]->qdependquestion = 'q'.$dependquestion->id;$question->dependencies[$did]->qdependchoice = $qdependchoice;$question->dependencies[$did]->parenttype = $dependquestion->type_id;// Other fields to be used in Questions edit mode.$question->dependencies[$did]->position = $question->position;$question->dependencies[$did]->name = $question->name;$question->dependencies[$did]->content = $question->content;$question->dependencies[$did]->parentposition = $dependquestion->position;$question->dependencies[$did]->parent = $dependquestion->name.'->'.$dependchoice;}return true;}/*** Determine the next valid page and return it. Return false if no valid next page.* @param int $secnum* @param int $rid* @return int | bool*/public function next_page($secnum, $rid) {$secnum++;$numsections = isset($this->questionsbysec) ? count($this->questionsbysec) : 0;if ($this->has_dependencies()) {while (!$this->eligible_questions_on_page($secnum, $rid)) {$secnum++;// We have reached the end of questionnaire on a page without any question left.if ($secnum > $numsections) {$secnum = false;break;}}}return $secnum;}/*** Determine the previous valid page and return it. Return false if no valid previous page.* @param int $secnum* @param int $rid* @return int | bool*/public function prev_page($secnum, $rid) {$secnum--;if ($this->has_dependencies()) {while (($secnum > 0) && !$this->eligible_questions_on_page($secnum, $rid)) {$secnum--;}}if ($secnum === 0) {$secnum = false;}return $secnum;}/*** Return the correct action to a next page request.* @param mod_questionnaire\responsetype\response\response $response* @param int $userid* @return bool|int|string*/public function next_page_action($response, $userid) {$msg = $this->response_check_format($response->sec, $response);if (empty($msg)) {$response->rid = $this->existing_response_action($response, $userid);return $this->next_page($response->sec, $response->rid);} else {return $msg;}}/*** Return the correct action to a previous page request.* @param mod_questionnaire\responsetype\response\response $response* @param int $userid* @return bool|int*/public function previous_page_action($response, $userid) {$response->rid = $this->existing_response_action($response, $userid);return $this->prev_page($response->sec, $response->rid);}/*** Handle updating an existing response.* @param mod_questionnaire\responsetype\response\response $response* @param int $userid* @return bool|int*/public function existing_response_action($response, $userid) {$this->response_delete($response->rid, $response->sec);return $this->response_insert($response, $userid);}/*** Are there any eligible questions to be displayed on the specified page/section.* @param int $secnum The section number to check.* @param int $rid The current response id.* @return boolean*/public function eligible_questions_on_page($secnum, $rid) {$questionstodisplay = false;foreach ($this->questionsbysec[$secnum] as $questionid) {if ($this->questions[$questionid]->dependency_fulfilled($rid, $this->questions)) {$questionstodisplay = true;break;}}return $questionstodisplay;}// Display Methods./*** The main display method for the survey. Adds HTML to the templates.* @param int $quser* @param bool $userid* @return string|void*/public function print_survey($quser, $userid=false) {global $SESSION, $CFG;if (!($formdata = data_submitted()) || !confirm_sesskey()) {$formdata = new stdClass();}$formdata->rid = $this->get_latest_responseid($quser);// If student saved a "resume" questionnaire OR left a questionnaire unfinished// and there are more pages than one find the page of the last answered question.if (($formdata->rid != 0) && (empty($formdata->sec) || intval($formdata->sec) < 1)) {$formdata->sec = $this->response_select_max_sec($formdata->rid);}if (empty($formdata->sec)) {$formdata->sec = 1;} else {$formdata->sec = (intval($formdata->sec) > 0) ? intval($formdata->sec) : 1;}$numsections = isset($this->questionsbysec) ? count($this->questionsbysec) : 0; // Indexed by section.$msg = '';$action = $CFG->wwwroot.'/mod/questionnaire/complete.php?id='.$this->cm->id;// TODO - Need to rework this. Too much crossover with ->view method.// Skip logic :: if this is page 1, it cannot be the end page with no questions on it!if ($formdata->sec == 1) {$SESSION->questionnaire->end = false;}if (!empty($formdata->submit)) {// Skip logic: we have reached the last page without any questions on it.if (isset($SESSION->questionnaire->end) && $SESSION->questionnaire->end == true) {return;}$msg = $this->response_check_format($formdata->sec, $formdata);if (empty($msg)) {return;}$formdata->rid = $this->existing_response_action($formdata, $userid);}if (!empty($formdata->resume) && ($this->resume)) {$this->response_delete($formdata->rid, $formdata->sec);$formdata->rid = $this->response_insert($formdata, $quser, true);$this->response_goto_saved($action);return;}// Save each section 's $formdata somewhere in case user returns to that page when navigating the questionnaire.if (!empty($formdata->next)) {$msg = $this->response_check_format($formdata->sec, $formdata);if ($msg) {$formdata->next = '';$formdata->rid = $this->existing_response_action($formdata, $userid);} else {$nextsec = $this->next_page_action($formdata, $userid);if ($nextsec === false) {$SESSION->questionnaire->end = true; // End of questionnaire reached on a no questions page.$formdata->sec = $numsections + 1;} else {$formdata->sec = $nextsec;}}}if (!empty($formdata->prev)) {// If skip logic and this is last page reached with no questions,// unlock questionnaire->end to allow navigate back to previous page.if (isset($SESSION->questionnaire->end) && ($SESSION->questionnaire->end == true)) {$SESSION->questionnaire->end = false;$formdata->sec--;}// Prevent navigation to previous page if wrong format in answered questions).$msg = $this->response_check_format($formdata->sec, $formdata, false, true);if ($msg) {$formdata->prev = '';$formdata->rid = $this->existing_response_action($formdata, $userid);} else {$prevsec = $this->previous_page_action($formdata, $userid);if ($prevsec === false) {$formdata->sec = 0;} else {$formdata->sec = $prevsec;}}}if (!empty($formdata->rid)) {$this->add_response($formdata->rid);}$formdatareferer = !empty($formdata->referer) ? htmlspecialchars($formdata->referer) : '';$formdatarid = isset($formdata->rid) ? $formdata->rid : '0';$this->page->add_to_page('formstart', $this->renderer->complete_formstart($action, ['referer' => $formdatareferer,'a' => $this->id, 'sid' => $this->survey->id, 'rid' => $formdatarid, 'sec' => $formdata->sec, 'sesskey' => sesskey()]));if (isset($this->questions) && $numsections) { // Sanity check.$this->survey_render($formdata, $formdata->sec, $msg);$controlbuttons = [];if ($formdata->sec > 1) {$controlbuttons['prev'] = ['type' => 'submit', 'class' => 'btn btn-secondary control-button-prev','value' => '<< '.get_string('previouspage', 'questionnaire')];}if ($this->resume) {$controlbuttons['resume'] = ['type' => 'submit', 'class' => 'btn btn-secondary control-button-save','value' => get_string('save_and_exit', 'questionnaire')];}// Add a 'hidden' variable for the mod's 'view.php', and use a language variable for the submit button.if ($formdata->sec == $numsections) {$controlbuttons['submittype'] = ['type' => 'hidden', 'value' => 'Submit Survey'];$controlbuttons['submit'] = ['type' => 'submit', 'class' => 'btn btn-primary control-button-submit','value' => get_string('submitsurvey', 'questionnaire')];} else {$controlbuttons['next'] = ['type' => 'submit', 'class' => 'btn btn-secondary control-button-next','value' => get_string('nextpage', 'questionnaire').' >>'];}$this->page->add_to_page('controlbuttons', $this->renderer->complete_controlbuttons($controlbuttons));} else {$this->page->add_to_page('controlbuttons',$this->renderer->complete_controlbuttons(get_string('noneinuse', 'questionnaire')));}$this->page->add_to_page('formend', $this->renderer->complete_formend());return $msg;}/*** Print the entire survey page.* @param stdClass $formdata* @param int $section* @param string $message* @return bool|void*/private function survey_render(&$formdata, $section = 1, $message = '') {$this->usehtmleditor = null;if (empty($section)) {$section = 1;}$numsections = isset($this->questionsbysec) ? count($this->questionsbysec) : 0;if ($section > $numsections) {$formdata->sec = $numsections;$this->page->add_to_page('notifications',$this->renderer->notification(get_string('finished', 'questionnaire'), \core\output\notification::NOTIFY_WARNING));return(false); // Invalid section.}// Check to see if there are required questions.$hasrequired = $this->has_required($section);// Find out what question number we are on $i New fix for question numbering.$i = 0;if ($section > 1) {for ($j = 2; $j <= $section; $j++) {foreach ($this->questionsbysec[$j - 1] as $questionid) {if ($this->questions[$questionid]->type_id < QUESPAGEBREAK) {$i++;}}}}$this->print_survey_start($message, $section, $numsections, $hasrequired, '', 1);// Only show progress bar on questionnaires with more than one page.if ($this->progressbar && isset($this->questionsbysec) && count($this->questionsbysec) > 1) {$this->page->add_to_page('progressbar',$this->renderer->render_progress_bar($section, $this->questionsbysec));}foreach ($this->questionsbysec[$section] as $questionid) {if ($this->questions[$questionid]->type_id != QUESSECTIONTEXT) {$i++;}// Need questionnaire id to get the questionnaire object in sectiontext (Label) question class.$formdata->questionnaire_id = $this->id;if (isset($formdata->rid) && !empty($formdata->rid)) {$this->add_response($formdata->rid);} else {$this->add_response_from_formdata($formdata);}$this->page->add_to_page('questions',$this->renderer->question_output($this->questions[$questionid],(isset($this->responses[$formdata->rid]) ? $this->responses[$formdata->rid] : []),$i, $this->usehtmleditor, []));}$this->print_survey_end($section, $numsections);return;}/*** Print the start of the survey page.* @param string $message* @param int $section* @param int $numsections* @param bool $hasrequired* @param string $rid* @param bool $blankquestionnaire* @param string $outputtarget*/private function print_survey_start($message, $section, $numsections, $hasrequired, $rid='', $blankquestionnaire=false,$outputtarget = 'html') {global $CFG, $DB;require_once($CFG->libdir.'/filelib.php');$userid = '';$resp = '';$groupname = '';$currentgroupid = 0;$timesubmitted = '';// Available group modes (0 = no groups; 1 = separate groups; 2 = visible groups).if ($rid) {$courseid = $this->course->id;if ($resp = $DB->get_record('questionnaire_response', array('id' => $rid)) ) {if ($this->respondenttype == 'fullname') {$userid = $resp->userid;// Display name of group(s) that student belongs to... if questionnaire is set to Groups separate or visible.if (groups_get_activity_groupmode($this->cm, $this->course)) {if ($groups = groups_get_all_groups($courseid, $resp->userid)) {if (count($groups) == 1) {$group = current($groups);$currentgroupid = $group->id;$groupname = ' ('.get_string('group').': '.$group->name.')';} else {$groupname = ' ('.get_string('groups').': ';foreach ($groups as $group) {$groupname .= $group->name.', ';}$groupname = substr($groupname, 0, strlen($groupname) - 2).')';}} else {$groupname = ' ('.get_string('groupnonmembers').')';}}$params = array('objectid' => $this->survey->id,'context' => $this->context,'courseid' => $this->course->id,'relateduserid' => $userid,'other' => array('action' => 'vresp', 'currentgroupid' => $currentgroupid, 'rid' => $rid));$event = \mod_questionnaire\event\response_viewed::create($params);$event->trigger();}}}$ruser = '';if ($resp && !$blankquestionnaire) {if ($userid) {if ($user = $DB->get_record('user', array('id' => $userid))) {$ruser = fullname($user);}}if ($this->respondenttype == 'anonymous') {$ruser = '- '.get_string('anonymous', 'questionnaire').' -';} else {// JR DEV comment following line out if you do NOT want time submitted displayed in Anonymous surveys.if ($resp->submitted) {$timesubmitted = ' '.get_string('submitted', 'questionnaire').' '.userdate($resp->submitted);}}}if ($ruser) {$respinfo = '';if ($outputtarget == 'html') {// Disable the pdf function for now, until it looks a lot better.if (false) {$linkname = get_string('downloadpdf', 'mod_questionnaire');$link = new moodle_url('/mod/questionnaire/report.php',['action' => 'vresp','instance' => $this->id,'target' => 'pdf','individualresponse' => 1,'rid' => $rid]);$downpdficon = new pix_icon('b/pdfdown', $linkname, 'mod_questionnaire');$respinfo .= $this->renderer->action_link($link, null, null, null, $downpdficon);}$linkname = get_string('print', 'mod_questionnaire');$link = new \moodle_url('/mod/questionnaire/report.php',['action' => 'vresp', 'instance' => $this->id, 'target' => 'print', 'individualresponse' => 1, 'rid' => $rid]);$htmlicon = new pix_icon('t/print', $linkname);$options = ['menubar' => true, 'location' => false, 'scrollbars' => true, 'resizable' => true,'height' => 600, 'width' => 800, 'title' => $linkname];$name = 'popup';$action = new popup_action('click', $link, $name, $options);$respinfo .= $this->renderer->action_link($link, null, $action, ['title' => $linkname], $htmlicon) . ' ';}$respinfo .= get_string('respondent', 'questionnaire').': <strong>'.$ruser.'</strong>';if ($this->survey_is_public()) {// For a public questionnaire, look for the course that used it.$coursename = '';$sql = 'SELECT q.id, q.course, c.fullname ' .'FROM {questionnaire_response} qr ' .'INNER JOIN {questionnaire} q ON qr.questionnaireid = q.id ' .'INNER JOIN {course} c ON q.course = c.id ' .'WHERE qr.id = ? AND qr.complete = ? ';if ($record = $DB->get_record_sql($sql, [$rid, 'y'])) {$coursename = $record->fullname;}$respinfo .= ' '.get_string('course'). ': '.$coursename;}$respinfo .= $groupname;$respinfo .= $timesubmitted;$this->page->add_to_page('respondentinfo', $this->renderer->respondent_info($respinfo));}// We don't want to display the print icon in the print popup window itself!if ($this->capabilities->printblank && $blankquestionnaire && $section == 1) {// Open print friendly as popup window.$linkname = ' '.get_string('printblank', 'questionnaire');$title = get_string('printblanktooltip', 'questionnaire');$url = '/mod/questionnaire/print.php?qid='.$this->id.'&rid=0&'.'courseid='.$this->course->id.'&sec=1';$options = array('menubar' => true, 'location' => false, 'scrollbars' => true, 'resizable' => true,'height' => 600, 'width' => 800, 'title' => $title);$name = 'popup';$link = new moodle_url($url);$action = new popup_action('click', $link, $name, $options);$class = "floatprinticon";$this->page->add_to_page('printblank',$this->renderer->action_link($link, $linkname, $action, array('class' => $class, 'title' => $title),new pix_icon('t/print', $title)));}if ($section == 1) {if (!empty($this->survey->title)) {$this->survey->title = format_string($this->survey->title);$this->page->add_to_page('title', $this->survey->title);}if (!empty($this->survey->subtitle)) {$this->survey->subtitle = format_string($this->survey->subtitle);$this->page->add_to_page('subtitle', $this->survey->subtitle);}if ($this->survey->info) {$infotext = file_rewrite_pluginfile_urls($this->survey->info, 'pluginfile.php',$this->context->id, 'mod_questionnaire', 'info', $this->survey->id);$this->page->add_to_page('addinfo', $infotext);}}if ($message) {$this->page->add_to_page('message', $this->renderer->notification($message, \core\output\notification::NOTIFY_ERROR));}}/*** Print the end of the survey page.* @param int $section* @param int $numsections*/private function print_survey_end($section, $numsections) {// If no pages autonumbering.if (!$this->pages_autonumbered()) {return;}if ($numsections > 1) {$a = new stdClass();$a->page = $section;$a->totpages = $numsections;$this->page->add_to_page('pageinfo',$this->renderer->container(get_string('pageof', 'questionnaire', $a).' ', 'surveyPage'));}}/*** Display a survey suitable for printing.* @param int $courseid* @param string $message* @param string $referer* @param int $rid* @param bool $blankquestionnaire If we are printing a blank questionnaire.* @return false|void*/public function survey_print_render($courseid, $message = '', $referer='', $rid=0, $blankquestionnaire=false) {global $DB, $CFG;if (! $course = $DB->get_record("course", array("id" => $courseid))) {throw new \moodle_exception('incorrectcourseid', 'mod_questionnaire');}$this->course = $course;if (!empty($rid)) {// If we're viewing a response, use this method.$this->view_response($rid, $referer);return;}if (empty($section)) {$section = 1;}if (isset($this->questionsbysec)) {$numsections = count($this->questionsbysec);} else {$numsections = 0;}if ($section > $numsections) {return(false); // Invalid section.}$hasrequired = $this->has_required();// Find out what question number we are on $i.$i = 1;for ($j = 2; $j <= $section; $j++) {$i += count($this->questionsbysec[$j - 1]);}$action = $CFG->wwwroot.'/mod/questionnaire/preview.php?id='.$this->cm->id;$this->page->add_to_page('formstart',$this->renderer->complete_formstart($action));// Print all sections.$formdata = new stdClass();$errors = 1;if (data_submitted()) {$formdata = data_submitted();$formdata->rid = $formdata->rid ?? 0;$this->add_response_from_formdata($formdata);$pageerror = '';$s = 1;$errors = 0;foreach ($this->questionsbysec as $section) {$errormessage = $this->response_check_format($s, $formdata);if ($errormessage) {if ($numsections > 1) {$pageerror = get_string('page', 'questionnaire').' '.$s.' : ';}$this->page->add_to_page('notifications',$this->renderer->notification($pageerror.$errormessage, \core\output\notification::NOTIFY_ERROR));$errors++;}$s ++;}}$this->print_survey_start($message, 1, 1, $hasrequired, '');if (($referer == 'preview') && $this->has_dependencies()) {$allqdependants = $this->get_dependants_and_choices();} else {$allqdependants = [];}if ($errors == 0) {$this->page->add_to_page('message',$this->renderer->notification(get_string('submitpreviewcorrect', 'questionnaire'),\core\output\notification::NOTIFY_SUCCESS));}$page = 1;foreach ($this->questionsbysec as $section) {$output = '';if ($numsections > 1) {$output .= $this->renderer->print_preview_pagenumber(get_string('page', 'questionnaire').' '.$page);$page++;}foreach ($section as $questionid) {if ($this->questions[$questionid]->type_id == QUESSECTIONTEXT) {$i--;}if (isset($allqdependants[$questionid])) {$dependants = $allqdependants[$questionid];} else {$dependants = [];}$output .= $this->renderer->question_output($this->questions[$questionid], $this->responses[0] ?? [],$i++, null, $dependants);$this->page->add_to_page('questions', $output);$output = '';}}// End of questions.if ($referer == 'preview' && !$blankquestionnaire) {$url = $CFG->wwwroot.'/mod/questionnaire/preview.php?id='.$this->cm->id;$this->page->add_to_page('formend',$this->renderer->print_preview_formend($url, get_string('submitpreview', 'questionnaire'), get_string('reset')));}return;}/*** Update an existing survey.* @param stdClass $sdata* @return bool|int*/public function survey_update($sdata) {global $DB;$errstr = ''; // TODO: notused!// New survey.if (empty($this->survey->id)) {// Create a new survey in the database.$fields = array('name', 'realm', 'title', 'subtitle', 'email', 'theme', 'thanks_page', 'thank_head','thank_body', 'feedbacknotes', 'info', 'feedbacksections', 'feedbackscores', 'chart_type');// Theme field deprecated.$record = new stdClass();$record->id = 0;$record->courseid = $sdata->courseid;foreach ($fields as $f) {if (isset($sdata->$f)) {$record->$f = $sdata->$f;}}$this->survey = new stdClass();$this->survey->id = $DB->insert_record('questionnaire_survey', $record);$this->add_survey($this->survey->id);if (!$this->survey->id) {$errstr = get_string('errnewname', 'questionnaire') .' [ : ]'; // TODO: notused!return(false);}} else {if (empty($sdata->name) || empty($sdata->title) || empty($sdata->realm)) {return(false);}if (!isset($sdata->chart_type)) {$sdata->chart_type = '';}$fields = array('name', 'realm', 'title', 'subtitle', 'email', 'theme', 'thanks_page','thank_head', 'thank_body', 'feedbacknotes', 'info', 'feedbacksections', 'feedbackscores', 'chart_type');$name = $DB->get_field('questionnaire_survey', 'name', array('id' => $this->survey->id));// Trying to change survey name.if (trim($name) != trim(stripslashes($sdata->name))) { // Var $sdata will already have slashes added to it.$count = $DB->count_records('questionnaire_survey', array('name' => $sdata->name));if ($count != 0) {$errstr = get_string('errnewname', 'questionnaire'); // TODO: notused!return(false);}}// UPDATE the row in the DB with current values.$surveyrecord = new stdClass();$surveyrecord->id = $this->survey->id;foreach ($fields as $f) {if (isset($sdata->{$f})) {$surveyrecord->$f = trim($sdata->{$f});}}$result = $DB->update_record('questionnaire_survey', $surveyrecord);if (!$result) {$errstr = get_string('warning', 'questionnaire').' [ : ]'; // TODO: notused!return(false);}}return($this->survey->id);}/*** Creates an editable copy of a survey.* @param int $owner* @return bool|int*/public function survey_copy($owner) {global $DB;// Clear the sid, clear the creation date, change the name, and clear the status.$survey = clone($this->survey);unset($survey->id);$survey->courseid = $owner;// Make sure that the survey name is not larger than the field size (CONTRIB-2999). Leave room for extra chars.$survey->name = core_text::substr($survey->name, 0, (64 - 10));$survey->name .= '_copy';$survey->status = 0;// Check for 'name' conflict, and resolve.$i = 0;$name = $survey->name;while ($DB->count_records('questionnaire_survey', array('name' => $name)) > 0) {$name = $survey->name.(++$i);}if ($i) {$survey->name .= $i;}// Create new survey.if (!($newsid = $DB->insert_record('questionnaire_survey', $survey))) {return(false);}// Make copies of all the questions.$pos = 1;// Skip logic: some changes needed here for dependencies down below.$qidarray = array();$cidarray = array();foreach ($this->questions as $question) {// Fix some fields first.$oldid = $question->id;unset($question->id);$question->surveyid = $newsid;$question->position = $pos++;// Copy question to new survey.if (!($newqid = $DB->insert_record('questionnaire_question', $question))) {return(false);}$qidarray[$oldid] = $newqid;foreach ($question->choices as $key => $choice) {$oldcid = $key;unset($choice->id);$choice->question_id = $newqid;if (!$newcid = $DB->insert_record('questionnaire_quest_choice', $choice)) {return(false);}$cidarray[$oldcid] = $newcid;}}// Replicate all dependency data.if ($dependquestions = $DB->get_records('questionnaire_dependency', ['surveyid' => $this->survey->id], 'questionid')) {foreach ($dependquestions as $dquestion) {$record = new stdClass();$record->questionid = $qidarray[$dquestion->questionid];$record->surveyid = $newsid;$record->dependquestionid = $qidarray[$dquestion->dependquestionid];// The response may not use choice id's (example boolean). If not, just copy the value.$responsetype = $this->questions[$dquestion->dependquestionid]->responsetype;if ($responsetype->transform_choiceid($dquestion->dependchoiceid) == $dquestion->dependchoiceid) {$record->dependchoiceid = $cidarray[$dquestion->dependchoiceid];} else {$record->dependchoiceid = $dquestion->dependchoiceid;}$record->dependlogic = $dquestion->dependlogic;$record->dependandor = $dquestion->dependandor;$DB->insert_record('questionnaire_dependency', $record);}}// Replicate any feedback data.// TODO: Need to handle image attachments (same for other copies above).if ($fbsections = $DB->get_records('questionnaire_fb_sections', ['surveyid' => $this->survey->id], 'id')) {foreach ($fbsections as $fbsid => $fbsection) {$fbsection->surveyid = $newsid;$scorecalculation = section::decode_scorecalculation($fbsection->scorecalculation);$newscorecalculation = [];foreach ($scorecalculation as $qid => $val) {$newscorecalculation[$qidarray[$qid]] = $val;}$fbsection->scorecalculation = serialize($newscorecalculation);unset($fbsection->id);$newfbsid = $DB->insert_record('questionnaire_fb_sections', $fbsection);if ($feedbackrecs = $DB->get_records('questionnaire_feedback', ['sectionid' => $fbsid], 'id')) {foreach ($feedbackrecs as $feedbackrec) {$feedbackrec->sectionid = $newfbsid;unset($feedbackrec->id);$DB->insert_record('questionnaire_feedback', $feedbackrec);}}}}return($newsid);}// RESPONSE LIBRARY./*** Check that all questions have been answered in a suitable way.* @param int $section* @param stdClass $formdata* @param bool $checkmissing* @param bool $checkwrongformat* @return string*/private function response_check_format($section, $formdata, $checkmissing = true, $checkwrongformat = true) {$missing = 0;$strmissing = ''; // Missing questions.$wrongformat = 0;$strwrongformat = ''; // Wrongly formatted questions (Numeric, 5:Check Boxes, Date).$i = 1;for ($j = 2; $j <= $section; $j++) {// ADDED A SIMPLE LOOP FOR MAKING SURE PAGE BREAKS (type 99) AND LABELS (type 100) ARE NOT ALLOWED.foreach ($this->questionsbysec[$j - 1] as $questionid) {$tid = $this->questions[$questionid]->type_id;if ($tid < QUESPAGEBREAK) {$i++;}}}$qnum = $i - 1;if (key_exists($section, $this->questionsbysec)) {foreach ($this->questionsbysec[$section] as $questionid) {$tid = $this->questions[$questionid]->type_id;if ($tid != QUESSECTIONTEXT) {$qnum++;}if (!$this->questions[$questionid]->response_complete($formdata)) {$missing++;$strnum = get_string('num', 'questionnaire') . $qnum . '. ';$strmissing .= $strnum;// Pop-up notification at the point of the error.$strnoti = get_string('missingquestion', 'questionnaire') . $strnum;$this->questions[$questionid]->add_notification($strnoti);}if (!$this->questions[$questionid]->response_valid($formdata)) {$wrongformat++;$strwrongformat .= get_string('num', 'questionnaire') . $qnum . '. ';}}}$message = '';$nonumbering = false;// If no questions autonumbering do not display missing question(s) number(s).if (!$this->questions_autonumbered()) {$nonumbering = true;}if ($checkmissing && $missing) {if ($nonumbering) {$strmissing = '';}if ($missing == 1) {$message = get_string('missingquestion', 'questionnaire').$strmissing;} else {$message = get_string('missingquestions', 'questionnaire').$strmissing;}if ($wrongformat) {$message .= '<br />';}}if ($checkwrongformat && $wrongformat) {if ($nonumbering) {$message .= get_string('wronganswers', 'questionnaire');} else {if ($wrongformat == 1) {$message .= get_string('wrongformat', 'questionnaire').$strwrongformat;} else {$message .= get_string('wrongformats', 'questionnaire').$strwrongformat;}}}return ($message);}/*** Delete the spcified response.* @param int $rid* @param null|int $sec*/private function response_delete($rid, $sec = null) {global $DB;if (empty($rid)) {return;}if ($sec != null) {if ($sec < 1) {return;}// Skip logic.$numsections = isset($this->questionsbysec) ? count($this->questionsbysec) : 0;$sec = min($numsections , $sec);/* get question_id's in this section */$qids = array();foreach ($this->questionsbysec[$sec] as $questionid) {$qids[] = $questionid;}if (empty($qids)) {return;} else {list($qsql, $params) = $DB->get_in_or_equal($qids);$qsql = ' AND question_id ' . $qsql;}} else {/* delete all */$qsql = '';$params = array();}/* delete values */$select = 'response_id = \'' . $rid . '\' ' . $qsql;foreach (array('response_bool', 'resp_single', 'resp_multiple', 'response_rank', 'response_text','response_other', 'response_date') as $tbl) {$DB->delete_records_select('questionnaire_'.$tbl, $select, $params);}}/*** Commit the specified response.* @param int $rid* @return bool*/private function response_commit($rid) {global $DB;$record = new stdClass();$record->id = $rid;$record->complete = 'y';$record->submitted = time();if ($this->grade < 0) {$record->grade = 1; // Don't know what to do if its a scale...} else {$record->grade = $this->grade;}return $DB->update_record('questionnaire_response', $record);}/*** Get the latest response id for the user, or verify that the given response id is valid.* @param int $userid* @return int*/public function get_latest_responseid($userid) {global $DB;// Find latest in progress rid.$params = ['questionnaireid' => $this->id, 'userid' => $userid, 'complete' => 'n'];if ($records = $DB->get_records('questionnaire_response', $params, 'submitted DESC', 'id,questionnaireid', 0, 1)) {$rec = reset($records);return $rec->id;} else {return 0;}}/*** Returns the number of the section in which questions have been answered in a response.* @param int $rid* @return int*/private function response_select_max_sec($rid) {global $DB;$pos = $this->response_select_max_pos($rid);$select = 'surveyid = ? AND type_id = ? AND position < ? AND deleted = ?';$params = [$this->sid, QUESPAGEBREAK, $pos, 'n'];$max = $DB->count_records_select('questionnaire_question', $select, $params) + 1;return $max;}/*** Returns the position of the last answered question in a response.* @param int $rid* @return int*/private function response_select_max_pos($rid) {global $DB;$max = 0;foreach (array('response_bool', 'resp_single', 'resp_multiple', 'response_rank', 'response_text','response_other', 'response_date') as $tbl) {$sql = 'SELECT MAX(q.position) as num FROM {questionnaire_'.$tbl.'} a, {questionnaire_question} q '.'WHERE a.response_id = ? AND '.'q.id = a.question_id AND '.'q.surveyid = ? AND '.'q.deleted = \'n\'';if ($record = $DB->get_record_sql($sql, array($rid, $this->sid))) {$newmax = (int)$record->num;if ($newmax > $max) {$max = $newmax;}}}return $max;}/*** Handle all submission notification actions.* @param int $rid The id of the response record.* @return boolean Operation success.**/private function submission_notify($rid) {global $DB;$success = true;if (isset($this->survey)) {if (isset($this->survey->email)) {$email = $this->survey->email;} else {$email = $DB->get_field('questionnaire_survey', 'email', ['id' => $this->survey->id]);}} else {$email = '';}if (!empty($email)) {$success = $this->response_send_email($rid, $email);}if (!empty($this->notifications)) {// Handle notification of submissions.$success = $this->send_submission_notifications($rid) && $success;}return $success;}/*** Send submission notifications to users with "submissionnotification" capability.* @param int $rid The id of the response record.* @return boolean Operation success.**/private function send_submission_notifications($rid) {global $CFG, $USER;$this->add_response($rid);$message = '';if ($this->notifications == 2) {$message .= $this->get_full_submission_for_notifications($rid);}$success = true;if ($notifyusers = $this->get_notifiable_users($USER->id)) {$info = new stdClass();// Need to handle user differently for anonymous surveys.if ($this->respondenttype != 'anonymous') {$info->userfrom = $USER;$info->username = fullname($info->userfrom, true);$info->profileurl = $CFG->wwwroot.'/user/view.php?id='.$info->userfrom->id.'&course='.$this->course->id;$langstringtext = 'submissionnotificationtextuser';$langstringhtml = 'submissionnotificationhtmluser';} else {$info->userfrom = \core_user::get_noreply_user();$info->username = '';$info->profileurl = '';$langstringtext = 'submissionnotificationtextanon';$langstringhtml = 'submissionnotificationhtmlanon';}$info->name = format_string($this->name);$info->submissionurl = $CFG->wwwroot.'/mod/questionnaire/report.php?action=vresp&sid='.$this->survey->id.'&rid='.$rid.'&instance='.$this->id;$info->coursename = $this->course->fullname;$info->postsubject = get_string('submissionnotificationsubject', 'questionnaire');$info->posttext = get_string($langstringtext, 'questionnaire', $info);$info->posthtml = '<p>' . get_string($langstringhtml, 'questionnaire', $info) . '</p>';if (!empty($message)) {$info->posttext .= html_to_text($message);$info->posthtml .= $message;}foreach ($notifyusers as $notifyuser) {$info->userto = $notifyuser;$this->send_message($info, 'notification');}}return $success;}/*** Message someone about something.** @param object $info The information for the message.* @param string $eventtype* @return void*/private function send_message($info, $eventtype) {$eventdata = new \core\message\message();$eventdata->courseid = $this->course->id;$eventdata->modulename = 'questionnaire';$eventdata->userfrom = $info->userfrom;$eventdata->userto = $info->userto;$eventdata->subject = $info->postsubject;$eventdata->fullmessage = $info->posttext;$eventdata->fullmessageformat = FORMAT_PLAIN;$eventdata->fullmessagehtml = $info->posthtml;$eventdata->smallmessage = $info->postsubject;$eventdata->name = $eventtype;$eventdata->component = 'mod_questionnaire';$eventdata->notification = 1;$eventdata->contexturl = $info->submissionurl;$eventdata->contexturlname = $info->name;message_send($eventdata);}/*** Returns a list of users that should receive notification about given submission.** @param int $userid The submission to grade* @return array*/public function get_notifiable_users($userid) {// Potential users should be active users only.$potentialusers = get_enrolled_users($this->context, 'mod/questionnaire:submissionnotification',null, 'u.*', null, null, null, true);$notifiableusers = [];if (groups_get_activity_groupmode($this->cm) == SEPARATEGROUPS) {if ($groups = groups_get_all_groups($this->course->id, $userid, $this->cm->groupingid)) {foreach ($groups as $group) {foreach ($potentialusers as $potentialuser) {if ($potentialuser->id == $userid) {// Do not send self.continue;}if (groups_is_member($group->id, $potentialuser->id)) {$notifiableusers[$potentialuser->id] = $potentialuser;}}}} else {// User not in group, try to find graders without group.foreach ($potentialusers as $potentialuser) {if ($potentialuser->id == $userid) {// Do not send self.continue;}if (!groups_has_membership($this->cm, $potentialuser->id)) {$notifiableusers[$potentialuser->id] = $potentialuser;}}}} else {foreach ($potentialusers as $potentialuser) {if ($potentialuser->id == $userid) {// Do not send self.continue;}$notifiableusers[$potentialuser->id] = $potentialuser;}}return $notifiableusers;}/*** Return a formatted string containing all the questions and answers for a specific submission.* @param int $rid* @return string*/private function get_full_submission_for_notifications($rid) {$responses = $this->get_full_submission_for_export($rid);$message = '';foreach ($responses as $response) {$message .= html_to_text($response->questionname) . "<br />\n";$message .= get_string('question') . ': ' . html_to_text($response->questiontext) . "<br />\n";$message .= get_string('answers', 'questionnaire') . ":<br />\n";foreach ($response->answers as $answer) {$message .= html_to_text($answer) . "<br />\n";}$message .= "<br />\n";}return $message;}/*** Construct the response data for a given response and return a structured export.* @param int $rid* @return string* @throws coding_exception*/public function get_structured_response($rid) {$this->add_response($rid);return $this->get_full_submission_for_export($rid);}/*** Return a JSON structure containing all the questions and answers for a specific submission.* @param int $rid* @return array*/private function get_full_submission_for_export($rid) {if (!isset($this->responses[$rid])) {$this->add_response($rid);}$exportstructure = [];foreach ($this->questions as $question) {$rqid = 'q' . $question->id;$response = new stdClass();$response->questionname = $question->position . '. ' . $question->name;$response->questiontext = $question->content;$response->answers = [];if ($question->type_id == 8) {$choices = [];$cids = [];foreach ($question->choices as $cid => $choice) {if (!empty($choice->value) && (strpos($choice->content, '=') !== false)) {$choices[$choice->value] = substr($choice->content, (strpos($choice->content, '=') + 1));} else {$cids[$rqid . '_' . $cid] = $choice->content;}}if (isset($this->responses[$rid]->answers[$question->id])) {foreach ($cids as $rqid => $choice) {$cid = substr($rqid, (strpos($rqid, '_') + 1));if (isset($this->responses[$rid]->answers[$question->id][$cid])) {if (isset($question->choices[$cid]) &&isset($choices[$this->responses[$rid]->answers[$question->id][$cid]->value])) {$rating = $choices[$this->responses[$rid]->answers[$question->id][$cid]->value];} else {$rating = $this->responses[$rid]->answers[$question->id][$cid]->value;}$response->answers[] = $question->choices[$cid]->content . ' = ' . $rating;}}}} else if ($question->has_choices()) {$answertext = '';if (isset($this->responses[$rid]->answers[$question->id])) {$i = 0;foreach ($this->responses[$rid]->answers[$question->id] as $answer) {if ($i > 0) {$answertext .= '; ';}if ($question->choices[$answer->choiceid]->is_other_choice()) {$answertext .= $answer->value;} else {$answertext .= $question->choices[$answer->choiceid]->content;}$i++;}}$response->answers[] = $answertext;} else if (isset($this->responses[$rid]->answers[$question->id])) {$response->answers[] = $this->responses[$rid]->answers[$question->id][0]->value;}$exportstructure[] = $response;}return $exportstructure;}/*** Format the submission answers for legacy email delivery.* @param array $answers The array of response answers.* @return array The formatted set of answers as plain text and HTML.*/private function get_formatted_answers_for_emails($answers) {global $USER;// Line endings for html and plaintext emails.$endhtml = "\r\n<br />";$endplaintext = "\r\n";reset($answers);$formatted = array('plaintext' => '', 'html' => '');for ($i = 0; $i < count($answers[0]); $i++) {$sep = ' : ';switch($i) {case 1:$sep = ' ';break;case 4:$formatted['plaintext'] .= get_string('user').' ';$formatted['html'] .= get_string('user').' ';break;case 6:if ($this->respondenttype != 'anonymous') {$formatted['html'] .= get_string('email').$sep.$USER->email. $endhtml;$formatted['plaintext'] .= get_string('email'). $sep. $USER->email. $endplaintext;}}$formatted['html'] .= $answers[0][$i].$sep.$answers[1][$i]. $endhtml;$formatted['plaintext'] .= $answers[0][$i].$sep.$answers[1][$i]. $endplaintext;}return $formatted;}/*** Send the full response submission to the defined email addresses.* @param int $rid The id of the response record.* @param string $email The comma separated list of emails to send to.* @return bool*/private function response_send_email($rid, $email) {global $CFG;$submission = $this->generate_csv(0, $rid, '', null, 1);if (!empty($submission)) {$answers = $this->get_formatted_answers_for_emails($submission);} else {$answers = ['html' => '', 'plaintext' => ''];}$name = s($this->name);if (empty($email)) {return(false);}// Line endings for html and plaintext emails.$endhtml = "\r\n<br>";$endplaintext = "\r\n";$subject = get_string('surveyresponse', 'questionnaire') .": $name [$rid]";$url = $CFG->wwwroot.'/mod/questionnaire/report.php?action=vresp&sid='.$this->survey->id.'&rid='.$rid.'&instance='.$this->id;// Html and plaintext body.$bodyhtml = '<a href="'.$url.'">'.$url.'</a>'.$endhtml;$bodyplaintext = $url.$endplaintext;$bodyhtml .= get_string('surveyresponse', 'questionnaire') .' "'.$name.'"'.$endhtml;$bodyplaintext .= get_string('surveyresponse', 'questionnaire') .' "'.$name.'"'.$endplaintext;$bodyhtml .= $answers['html'];$bodyplaintext .= $answers['plaintext'];// Use plaintext version for altbody.$altbody = "\n$bodyplaintext\n";$return = true;$mailaddresses = preg_split('/,|;/', $email);foreach ($mailaddresses as $email) {$userto = new stdClass();$userto->email = trim($email);$userto->mailformat = 1;// Dummy userid to keep email_to_user happy in moodle 2.6.$userto->id = -10;$userfrom = $CFG->noreplyaddress;if (email_to_user($userto, $userfrom, $subject, $altbody, $bodyhtml)) {$return = $return && true;} else {$return = false;}}return $return;}/*** Insert the provided response.* @param object $responsedata An object containing all data for the response.* @param int $userid* @param bool $resume* @return bool|int*/public function response_insert($responsedata, $userid, $resume=false) {global $DB;$record = new stdClass();$record->submitted = time();if (empty($responsedata->rid)) {// Create a uniqe id for this response.$record->questionnaireid = $this->id;$record->userid = $userid;$responsedata->rid = $DB->insert_record('questionnaire_response', $record);$responsedata->id = $responsedata->rid;} else {$record->id = $responsedata->rid;$DB->update_record('questionnaire_response', $record);}if ($resume) {// Log this saved response.// Needed for the event logging.$context = context_module::instance($this->cm->id);$anonymous = $this->respondenttype == 'anonymous';$params = array('context' => $context,'courseid' => $this->course->id,'relateduserid' => $userid,'anonymous' => $anonymous,'other' => array('questionnaireid' => $this->id));$event = \mod_questionnaire\event\attempt_saved::create($params);$event->trigger();}if (!isset($responsedata->sec)) {$responsedata->sec = 1;}if (!empty($this->questionsbysec[$responsedata->sec])) {foreach ($this->questionsbysec[$responsedata->sec] as $questionid) {$this->questions[$questionid]->insert_response($responsedata);}}return($responsedata->rid);}/*** Get the answers for the all response types.* @param int $rid* @return array*/private function response_select($rid) {// Response_bool (yes/no).$values = \mod_questionnaire\responsetype\boolean::response_select($rid);// Response_single (radio button or dropdown).$values += \mod_questionnaire\responsetype\single::response_select($rid);// Response_multiple.$values += \mod_questionnaire\responsetype\multiple::response_select($rid);// Response_rank.$values += \mod_questionnaire\responsetype\rank::response_select($rid);// Response_text.$values += \mod_questionnaire\responsetype\text::response_select($rid);// Response_date.$values += \mod_questionnaire\responsetype\date::response_select($rid);return($values);}/*** Redirect to the appropriate finish page.*/private function response_goto_thankyou() {global $CFG, $USER, $DB;$select = 'id = '.$this->survey->id;$fields = 'thanks_page, thank_head, thank_body';if ($result = $DB->get_record_select('questionnaire_survey', $select, null, $fields)) {$thankurl = $result->thanks_page;$thankhead = $result->thank_head;$thankbody = $result->thank_body;} else {$thankurl = '';$thankhead = '';$thankbody = '';}if (!empty($thankurl)) {if (!headers_sent()) {header("Location: $thankurl");exit;}echo '<script language="JavaScript" type="text/javascript"><!--window.location="'.$thankurl.'"//--></script><noscript><h2 class="thankhead">Thank You for completing this survey.</h2><blockquote class="thankbody">Please click<a href="'.$thankurl.'">here</a> to continue.</blockquote></noscript>';exit;}if (empty($thankhead)) {$thankhead = get_string('thank_head', 'questionnaire');}if ($this->progressbar && isset($this->questionsbysec) && count($this->questionsbysec) > 1) {// Show 100% full progress bar on completion.$this->page->add_to_page('progressbar',$this->renderer->render_progress_bar(count($this->questionsbysec) + 1, $this->questionsbysec));}$this->page->add_to_page('title', $thankhead);$this->page->add_to_page('addinfo',format_text(file_rewrite_pluginfile_urls($thankbody, 'pluginfile.php',$this->context->id, 'mod_questionnaire', 'thankbody', $this->survey->id), FORMAT_HTML, ['noclean' => true]));// Default set currentgroup to view all participants.// TODO why not set to current respondent's groupid (if any)?$currentgroupid = 0;$currentgroupid = groups_get_activity_group($this->cm);if (!groups_is_member($currentgroupid, $USER->id)) {$currentgroupid = 0;}if ($this->capabilities->readownresponses) {$url = new moodle_url('myreport.php', ['id' => $this->cm->id, 'instance' => $this->cm->instance, 'user' => $USER->id,'byresponse' => 0, 'action' => 'vresp']);$this->page->add_to_page('continue', $this->renderer->single_button($url, get_string('continue')));} else {$url = new moodle_url('/course/view.php', ['id' => $this->course->id]);$this->page->add_to_page('continue', $this->renderer->single_button($url, get_string('continue')));}return;}/*** Redirect to the provided url.* @param string $url*/private function response_goto_saved($url) {global $CFG, $USER;$resumesurvey = get_string('resumesurvey', 'questionnaire');$savedprogress = get_string('savedprogress', 'questionnaire', '<strong>'.$resumesurvey.'</strong>');$this->page->add_to_page('notifications',$this->renderer->notification($savedprogress, \core\output\notification::NOTIFY_SUCCESS));$this->page->add_to_page('respondentinfo',$this->renderer->homelink($CFG->wwwroot.'/course/view.php?id='.$this->course->id,get_string("backto", "moodle", $this->course->fullname)));if ($this->resume) {$message = $this->user_access_messages($USER->id, true);if ($message === false) {if ($this->user_can_take($USER->id)) {if ($this->questions) { // Sanity check.if ($this->user_has_saved_response($USER->id)) {$this->page->add_to_page('respondentinfo',$this->renderer->homelink($CFG->wwwroot . '/mod/questionnaire/complete.php?' .'id=' . $this->cm->id . '&resume=1', $resumesurvey));}}}}}return;}// Survey Results Methods./*** Add the navigation to the responses page.* @param int $currrid* @param int $currentgroupid* @param stdClass $cm* @param bool $byresponse*/public function survey_results_navbar_alpha($currrid, $currentgroupid, $cm, $byresponse) {global $CFG, $DB;// Is this questionnaire set to fullname or anonymous?$isfullname = $this->respondenttype != 'anonymous';if ($isfullname) {$responses = $this->get_responses(false, $currentgroupid);} else {$responses = $this->get_responses();}if (!$responses) {return;}$total = count($responses);if ($total === 0) {return;}$rids = array();if ($isfullname) {$ridssub = array();$ridsuserfullname = array();$ridsuserid = array();}$i = 0;$currpos = -1;foreach ($responses as $response) {array_push($rids, $response->id);if ($isfullname) {$user = $DB->get_record('user', array('id' => $response->userid));array_push($ridssub, $response->submitted);array_push($ridsuserfullname, fullname($user));array_push($ridsuserid, $response->userid);}if ($response->id == $currrid) {$currpos = $i;}$i++;}$url = $CFG->wwwroot.'/mod/questionnaire/report.php?action=vresp&group='.$currentgroupid.'&individualresponse=1';if (!$byresponse) { // Display navbar.// Build navbar.$navbar = new \stdClass();$prevrid = ($currpos > 0) ? $rids[$currpos - 1] : null;$nextrid = ($currpos < $total - 1) ? $rids[$currpos + 1] : null;$firstrid = $rids[0];$lastrid = $rids[$total - 1];if ($prevrid != null) {$pos = $currpos - 1;$title = '';$firstuserfullname = '';$navbar->firstrespondent = ['url' => ($url.'&rid='.$firstrid)];$navbar->previous = ['url' => ($url.'&rid='.$prevrid)];if ($isfullname) {$responsedate = userdate($ridssub[$pos]);$title = $ridsuserfullname[$pos];// Only add date if more than one response by a student.if ($ridsuserid[$pos] == $ridsuserid[$currpos]) {$title .= ' | '.$responsedate;}$firstuserfullname = $ridsuserfullname[0];}$navbar->firstrespondent['title'] = $firstuserfullname;$navbar->previous['title'] = $title;}$navbar->respnumber = ['currpos' => ($currpos + 1), 'total' => $total];if ($nextrid != null) {$pos = $currpos + 1;$responsedate = '';$title = '';$lastuserfullname = '';$navbar->lastrespondent = ['url' => ($url.'&rid='.$lastrid)];$navbar->next = ['url' => ($url.'&rid='.$nextrid)];if ($isfullname) {$responsedate = userdate($ridssub[$pos]);$title = $ridsuserfullname[$pos];// Only add date if more than one response by a student.if ($ridsuserid[$pos] == $ridsuserid[$currpos]) {$title .= ' | '.$responsedate;}$lastuserfullname = $ridsuserfullname[$total - 1];}$navbar->lastrespondent['title'] = $lastuserfullname;$navbar->next['title'] = $title;}$url = $CFG->wwwroot.'/mod/questionnaire/report.php?action=vresp&byresponse=1&group='.$currentgroupid;// Display navbar.$navbar->listlink = $url;// Display a "print this response" icon here in prevision of total removal of tabs in version 2.6.$linkname = ' '.get_string('print', 'questionnaire');$url = '/mod/questionnaire/print.php?qid='.$this->id.'&rid='.$currrid.'&courseid='.$this->course->id.'&sec=1';$title = get_string('printtooltip', 'questionnaire');$options = array('menubar' => true, 'location' => false, 'scrollbars' => true,'resizable' => true, 'height' => 600, 'width' => 800);$name = 'popup';$link = new moodle_url($url);$action = new popup_action('click', $link, $name, $options);$actionlink = $this->renderer->action_link($link, $linkname, $action, ['title' => $title],new pix_icon('t/print', $title));$navbar->printaction = $actionlink;$this->page->add_to_page('navigationbar', $this->renderer->navigationbar($navbar));} else { // Display respondents list.$resparr = [];for ($i = 0; $i < $total; $i++) {if ($isfullname) {$responsedate = userdate($ridssub[$i]);$resparr[] = '<a title = "'.$responsedate.'" href="'.$url.'&rid='.$rids[$i].'&individualresponse=1" >'.$ridsuserfullname[$i].'</a> ';} else {$responsedate = '';$resparr[] = '<a title = "'.$responsedate.'" href="'.$url.'&rid='.$rids[$i].'&individualresponse=1" >'.get_string('response', 'questionnaire').($i + 1).'</a> ';}}// Table formatting from http://wikkawiki.org/PageAndCategoryDivisionInACategory.$total = count($resparr);$entries = count($resparr);// Default max 3 columns, max 25 lines per column.// TODO make this setting customizable.$maxlines = 20;$maxcols = 3;if ($entries >= $maxlines) {$colnumber = min (intval($entries / $maxlines), $maxcols);} else {$colnumber = 1;}$lines = 0;$a = 0;// How many lines with an entry in every column do we have?while ($entries / $colnumber > 1) {$lines++;$entries = $entries - $colnumber;}// Prepare output.$respcols = new stdClass();for ($i = 0; $i < $colnumber; $i++) {$colname = 'respondentscolumn'.$i;$respcols->$colname = (object)['respondentlink' => []];for ($j = 0; $j < $lines; $j++) {$respcols->{$colname}->respondentlink[] = $resparr[$a];$a++;}// The rest of the entries (less than the number of cols).if ($entries) {$respcols->{$colname}->respondentlink[] = $resparr[$a];$entries--;$a++;}}$this->page->add_to_page('responses', $this->renderer->responselist($respcols));}}/*** Display responses for current user (your responses).* @param int $currrid* @param int $userid* @param int $instance* @param array $resps* @param string $reporttype* @param string $sid*/public function survey_results_navbar_student($currrid, $userid, $instance, $resps, $reporttype='myreport', $sid='') {global $DB;$stranonymous = get_string('anonymous', 'questionnaire');$total = count($resps);$rids = array();$ridssub = array();$ridsusers = array();$i = 0;$currpos = -1;$title = '';foreach ($resps as $response) {array_push($rids, $response->id);array_push($ridssub, $response->submitted);$ruser = '';if ($reporttype == 'report') {if ($this->respondenttype != 'anonymous') {if ($user = $DB->get_record('user', ['id' => $response->userid])) {$ruser = ' | ' .fullname($user);}} else {$ruser = ' | ' . $stranonymous;}}array_push($ridsusers, $ruser);if ($response->id == $currrid) {$currpos = $i;}$i++;}$prevrid = ($currpos > 0) ? $rids[$currpos - 1] : null;$nextrid = ($currpos < $total - 1) ? $rids[$currpos + 1] : null;if ($reporttype == 'myreport') {$url = 'myreport.php?instance='.$instance.'&user='.$userid.'&action=vresp&byresponse=1&individualresponse=1';} else {$url = 'report.php?instance='.$instance.'&user='.$userid.'&action=vresp&byresponse=1&individualresponse=1&sid='.$sid;}$navbar = new \stdClass();$displaypos = 1;if ($prevrid != null) {$title = userdate($ridssub[$currpos - 1].$ridsusers[$currpos - 1]);$navbar->previous = ['url' => ($url.'&rid='.$prevrid), 'title' => $title];}for ($i = 0; $i < $currpos; $i++) {$title = userdate($ridssub[$i]).$ridsusers[$i];$navbar->prevrespnumbers[] = ['url' => ($url.'&rid='.$rids[$i]), 'title' => $title, 'respnumber' => $displaypos];$displaypos++;}$navbar->currrespnumber = $displaypos;for (++$i; $i < $total; $i++) {$displaypos++;$title = userdate($ridssub[$i]).$ridsusers[$i];$navbar->nextrespnumbers[] = ['url' => ($url.'&rid='.$rids[$i]), 'title' => $title, 'respnumber' => $displaypos];}if ($nextrid != null) {$title = userdate($ridssub[$currpos + 1]).$ridsusers[$currpos + 1];$navbar->next = ['url' => ($url.'&rid='.$nextrid), 'title' => $title];}$this->page->add_to_page('navigationbar', $this->renderer->usernavigationbar($navbar));$this->page->add_to_page('bottomnavigationbar', $this->renderer->usernavigationbar($navbar));}/*** Builds HTML for the results for the survey. If a question id and choice id(s) are given, then the results are only calculated* for respodants who chose from the choice ids for the given question id. Returns empty string on success, else returns an* error string.* @param string $rid* @param bool $uid* @param bool $pdf* @param string $currentgroupid* @param string $sort* @return string|void*/public function survey_results($rid = '', $uid=false, $pdf = false, $currentgroupid='', $sort='') {global $SESSION, $DB;$SESSION->questionnaire->noresponses = false;// Build associative array holding whether each question// type has answer choices or not and the table the answers are in// TO DO - FIX BELOW TO USE STANDARD FUNCTIONS.$haschoices = array();$responsetable = array();if (!($types = $DB->get_records('questionnaire_question_type', array(), 'typeid', 'typeid, has_choices, response_table'))) {$errmsg = sprintf('%s [ %s: question_type ]',get_string('errortable', 'questionnaire'), 'Table');return($errmsg);}foreach ($types as $type) {$haschoices[$type->typeid] = $type->has_choices; // TODO is that variable actually used?$responsetable[$type->typeid] = $type->response_table;}// Load survey title (and other globals).if (empty($this->survey)) {$errmsg = get_string('erroropening', 'questionnaire') ." [ ID:{$this->sid} R:";return($errmsg);}if (empty($this->questions)) {$errmsg = get_string('erroropening', 'questionnaire') .' '. 'No questions found.';return($errmsg);}// Find total number of survey responses and relevant response ID's.if (!empty($rid)) {$rids = $rid;if (is_array($rids)) {$navbar = false;} else {$navbar = true;}$numresps = 1;} else {$navbar = false;if ($uid !== false) { // One participant only.$rows = $this->get_responses($uid);// All participants or all members of a group.} else if ($currentgroupid == 0) {$rows = $this->get_responses();} else { // Members of a specific group.$rows = $this->get_responses(false, $currentgroupid);}if (!$rows) {$this->page->add_to_page('respondentinfo',$this->renderer->notification(get_string('noresponses', 'questionnaire'),\core\output\notification::NOTIFY_ERROR));$SESSION->questionnaire->noresponses = true;return;}$numresps = count($rows);$this->page->add_to_page('respondentinfo',' '.get_string('responses', 'questionnaire').': <strong>'.$numresps.'</strong>');if (empty($rows)) {$errmsg = get_string('erroropening', 'questionnaire') .' '. get_string('noresponsedata', 'questionnaire');return($errmsg);}$rids = array();foreach ($rows as $row) {array_push($rids, $row->id);}}if ($navbar) {// Show response navigation bar.$this->survey_results_navbar($rid);}$this->page->add_to_page('title', format_string($this->survey->title));if ($this->survey->subtitle) {$this->page->add_to_page('subtitle', format_string($this->survey->subtitle));}if ($this->survey->info) {$infotext = file_rewrite_pluginfile_urls($this->survey->info, 'pluginfile.php',$this->context->id, 'mod_questionnaire', 'info', $this->survey->id);$this->page->add_to_page('addinfo', format_text($infotext, FORMAT_HTML, ['noclean' => true]));}$qnum = 0;$anonymous = $this->respondenttype == 'anonymous';foreach ($this->questions as $question) {if ($question->type_id == QUESPAGEBREAK) {continue;}if ($question->type_id != QUESSECTIONTEXT) {$qnum++;}if (!$pdf) {$this->page->add_to_page('responses', $this->renderer->container_start('qn-container'));$this->page->add_to_page('responses', $this->renderer->container_start('qn-info'));if ($question->type_id != QUESSECTIONTEXT) {$this->page->add_to_page('responses', $this->renderer->heading($qnum, 2, 'qn-number'));}$this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-info.$this->page->add_to_page('responses', $this->renderer->container_start('qn-content'));}// If question text is "empty", i.e. 2 non-breaking spaces were inserted, do not display any question text.if ($question->content == '<p>Â Â </p>') {$question->content = '';}if ($pdf) {$response = new stdClass();if ($question->type_id != QUESSECTIONTEXT) {$response->qnum = $qnum;}$response->qcontent = format_text(file_rewrite_pluginfile_urls($question->content, 'pluginfile.php',$question->context->id, 'mod_questionnaire', 'question', $question->id),FORMAT_HTML, ['noclean' => true]);$response->results = $this->renderer->results_output($question, $rids, $sort, $anonymous, $pdf);$this->page->add_to_page('responses', $response);} else {$this->page->add_to_page('responses',$this->renderer->container(format_text(file_rewrite_pluginfile_urls($question->content, 'pluginfile.php',$question->context->id, 'mod_questionnaire', 'question', $question->id),FORMAT_HTML, ['noclean' => true]), 'qn-question'));$this->page->add_to_page('responses', $this->renderer->results_output($question, $rids, $sort, $anonymous));$this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-content.$this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-container.}}return;}/*** Get unique list of question types used in the current survey.* author: Guy Thomas* @param bool $uniquebytable* @return array*/protected function get_survey_questiontypes($uniquebytable = false) {$uniquetypes = [];$uniquetables = [];foreach ($this->questions as $question) {$type = $question->type_id;$responsetable = $question->responsetable;// Build SQL for this question type if not already done.if (!$uniquebytable || !in_array($responsetable, $uniquetables)) {if (!in_array($type, $uniquetypes)) {$uniquetypes[] = $type;}if (!in_array($responsetable, $uniquetables)) {$uniquetables[] = $responsetable;}}}return $uniquetypes;}/*** Return array of all types considered to be choices.** @return array*/protected function choice_types() {return [QUESRADIO, QUESDROP, QUESCHECK, QUESRATE];}/*** Return all the fields to be used for users in questionnaire sql.* author: Guy Thomas* @return array|string*/protected function user_fields() {if (class_exists('\core_user\fields')) {$userfieldsarr = \core_user\fields::get_name_fields();} else {$userfieldsarr = get_all_user_name_fields();}$userfieldsarr = array_merge($userfieldsarr, ['username', 'department', 'institution']);return $userfieldsarr;}/*** Get all survey responses in one go.* author: Guy Thomas* @param string $rid* @param string $userid* @param bool $groupid* @param int $showincompletes* @return array*/protected function get_survey_all_responses($rid = '', $userid = '', $groupid = false, $showincompletes = 0) {global $DB;$uniquetypes = $this->get_survey_questiontypes(true);$allresponsessql = "";$allresponsesparams = [];// If a questionnaire is "public", and this is the master course, need to get responses from all instances.if ($this->survey_is_public_master()) {$qids = array_keys($DB->get_records('questionnaire', ['sid' => $this->sid], 'id') ?? []);} else {$qids = $this->id;}foreach ($uniquetypes as $type) {$question = \mod_questionnaire\question\question::question_builder($type);if (!isset($question->responsetype)) {continue;}$allresponsessql .= $allresponsessql == '' ? '' : ' UNION ALL ';list ($sql, $params) = $question->responsetype->get_bulk_sql($qids, $rid, $userid, $groupid, $showincompletes);$allresponsesparams = array_merge($allresponsesparams, $params);$allresponsessql .= $sql;}$allresponsessql .= " ORDER BY usrid, id";$allresponses = $DB->get_recordset_sql($allresponsessql, $allresponsesparams);return $allresponses ?? [];}/*** Return true if the survey is a 'public' one.** @return boolean*/public function survey_is_public() {return is_object($this->survey) && ($this->survey->realm == 'public');}/*** Return true if the survey is a 'public' one and this is the master instance.** @return boolean*/public function survey_is_public_master() {return $this->survey_is_public() && ($this->course->id == $this->survey->courseid);}/*** Process individual row for csv output* @param array $row* @param stdClass $resprow resultset row* @param int $currentgroupid* @param array $questionsbyposition* @param int $nbinfocols* @param int $numrespcols* @param int $showincompletes* @return array*/protected function process_csv_row(array &$row,stdClass $resprow,$currentgroupid,array &$questionsbyposition,$nbinfocols,$numrespcols, $showincompletes = 0) {global $DB;static $config = null;// If using an anonymous response, map users to unique user numbers so that number of unique anonymous users can be seen.static $anonumap = [];if ($config === null) {$config = get_config('questionnaire', 'downloadoptions');}$options = empty($config) ? array() : explode(',', $config);if ($showincompletes == 1) {$options[] = 'complete';}$positioned = [];$user = new stdClass();foreach ($this->user_fields() as $userfield) {$user->$userfield = $resprow->$userfield;}$user->id = $resprow->userid;$isanonymous = ($this->respondenttype == 'anonymous');// Moodle:// Get the course name that this questionnaire belongs to.if (!$this->survey_is_public()) {$courseid = $this->course->id;$coursename = $this->course->fullname;} else {// For a public questionnaire, look for the course that used it.$sql = 'SELECT q.id, q.course, c.fullname ' .'FROM {questionnaire_response} qr ' .'INNER JOIN {questionnaire} q ON qr.questionnaireid = q.id ' .'INNER JOIN {course} c ON q.course = c.id ' .'WHERE qr.id = ? AND qr.complete = ? ';if ($record = $DB->get_record_sql($sql, [$resprow->rid, 'y'])) {$courseid = $record->course;$coursename = $record->fullname;} else {$courseid = $this->course->id;$coursename = $this->course->fullname;}}// Moodle:// Determine if the user is a member of a group in this course or not.// TODO - review for performance.$groupname = '';if (groups_get_activity_groupmode($this->cm, $this->course)) {if ($currentgroupid > 0) {$groupname = groups_get_group_name($currentgroupid);} else {if ($user->id) {if ($groups = groups_get_all_groups($courseid, $user->id)) {foreach ($groups as $group) {$groupname .= $group->name.', ';}$groupname = substr($groupname, 0, strlen($groupname) - 2);} else {$groupname = ' ('.get_string('groupnonmembers').')';}}}}if ($isanonymous) {if (!isset($anonumap[$user->id])) {$anonumap[$user->id] = count($anonumap) + 1;}$fullname = get_string('anonymous', 'questionnaire') . $anonumap[$user->id];$username = '';$uid = '';} else {$uid = $user->id;$fullname = fullname($user);$username = $user->username;}if (in_array('response', $options)) {array_push($positioned, $resprow->rid);}if (in_array('submitted', $options)) {// For better compabitility & readability with Excel.$submitted = date(get_string('strfdateformatcsv', 'questionnaire'), $resprow->submitted);array_push($positioned, $submitted);}if (in_array('institution', $options)) {array_push($positioned, $user->institution);}if (in_array('department', $options)) {array_push($positioned, $user->department);}if (in_array('course', $options)) {array_push($positioned, $coursename);}if (in_array('group', $options)) {array_push($positioned, $groupname);}if (in_array('id', $options)) {array_push($positioned, $uid);}if (in_array('fullname', $options)) {array_push($positioned, $fullname);}if (in_array('username', $options)) {array_push($positioned, $username);}if (in_array('complete', $options)) {array_push($positioned, $resprow->complete);}for ($c = $nbinfocols; $c < $numrespcols; $c++) {if (isset($row[$c])) {$positioned[] = $row[$c];} else if (isset($questionsbyposition[$c])) {$question = $questionsbyposition[$c];$qtype = intval($question->type_id);if ($qtype === QUESCHECK) {$positioned[] = '0';} else {$positioned[] = null;}} else {$positioned[] = null;}}return $positioned;}/*** Exports the results of a survey to an array.* @param int $currentgroupid* @param string $rid* @param string $userid* @param int $choicecodes* @param int $choicetext* @param int $showincompletes* @param int $rankaverages* @return array*/public function generate_csv($currentgroupid, $rid='', $userid='', $choicecodes=1, $choicetext=0, $showincompletes=0,$rankaverages=0) {global $DB;raise_memory_limit('1G');$output = array();$stringother = get_string('other', 'questionnaire');$config = get_config('questionnaire', 'downloadoptions');$options = empty($config) ? array() : explode(',', $config);if ($showincompletes == 1) {$options[] = 'complete';}$columns = array();$types = array();foreach ($options as $option) {if (in_array($option, array('response', 'submitted', 'id'))) {$columns[] = get_string($option, 'questionnaire');$types[] = 0;} else {$columns[] = get_string($option);$types[] = 1;}}$nbinfocols = count($columns);$idtocsvmap = array('0', // 0: unused'0', // 1: bool -> boolean'1', // 2: text -> string'1', // 3: essay -> string'0', // 4: radio -> string'0', // 5: check -> string'0', // 6: dropdn -> string'0', // 7: rating -> number'0', // 8: rate -> number'1', // 9: date -> string'0', // 10: numeric -> number.'0', // 11: slider -> number.);if (!$survey = $DB->get_record('questionnaire_survey', array('id' => $this->survey->id))) {throw new \moodle_exception('surveynotexists', 'mod_questionnaire');}// Get all responses for this survey in one go.$allresponsesrs = $this->get_survey_all_responses($rid, $userid, $currentgroupid, $showincompletes);// Do we have any questions of type RADIO, DROP, CHECKBOX OR RATE? If so lets get all their choices in one go.$choicetypes = $this->choice_types();// Get unique list of question types used in this survey.$uniquetypes = $this->get_survey_questiontypes();if (count(array_intersect($choicetypes, $uniquetypes)) > 0) {$choiceparams = [$this->survey->id];$choicesql = "SELECT DISTINCT c.id as cid, q.id as qid, q.precise AS precise, q.name, c.contentFROM {questionnaire_question} qJOIN {questionnaire_quest_choice} c ON question_id = q.idWHERE q.surveyid = ? ORDER BY cid ASC";$choicerecords = $DB->get_records_sql($choicesql, $choiceparams);$choicesbyqid = [];if (!empty($choicerecords)) {// Hash the options by question id.foreach ($choicerecords as $choicerecord) {if (!isset($choicesbyqid[$choicerecord->qid])) {// New question id detected, intialise empty array to store choices.$choicesbyqid[$choicerecord->qid] = [];}$choicesbyqid[$choicerecord->qid][$choicerecord->cid] = $choicerecord;}}}$num = 1;$questionidcols = [];foreach ($this->questions as $question) {// Skip questions that aren't response capable.if (!isset($question->responsetype)) {continue;}// Establish the table's field names.$qid = $question->id;$qpos = $question->position;$col = $question->name;$type = $question->type_id;if (in_array($type, $choicetypes)) {/* single or multiple or rate */if (!isset($choicesbyqid[$qid])) {throw new coding_exception('Choice question has no choices!', 'question id '.$qid.' of type '.$type);}$choices = $choicesbyqid[$qid];switch ($type) {case QUESRADIO: // Single.case QUESDROP:$columns[][$qpos] = $col;$questionidcols[][$qpos] = $qid;array_push($types, $idtocsvmap[$type]);$thisnum = 1;foreach ($choices as $choice) {$content = $choice->content;// If "Other" add a column for the actual "other" text entered.if (\mod_questionnaire\question\choice::content_is_other_choice($content)) {$col = $choice->name.'_'.$stringother;$columns[][$qpos] = $col;$questionidcols[][$qpos] = null;array_push($types, '0');}}break;case QUESCHECK: // Multiple.$thisnum = 1;foreach ($choices as $choice) {$content = $choice->content;$modality = '';$contents = questionnaire_choice_values($content);if ($contents->modname) {$modality = $contents->modname;} else if ($contents->title) {$modality = $contents->title;} else {$modality = strip_tags($contents->text);}$col = $choice->name.'->'.$modality;$columns[][$qpos] = $col;$questionidcols[][$qpos] = $qid.'_'.$choice->cid;array_push($types, '0');// If "Other" add a column for the "other" checkbox.// Then add a column for the actual "other" text entered.if (\mod_questionnaire\question\choice::content_is_other_choice($content)) {$content = $stringother;$col = $choice->name.'->['.$content.']';$columns[][$qpos] = $col;$questionidcols[][$qpos] = null;array_push($types, '0');}}break;case QUESRATE: // Rate.foreach ($choices as $choice) {$nameddegrees = 0;$modality = '';$content = $choice->content;$osgood = false;if (\mod_questionnaire\question\rate::type_is_osgood_rate_scale($choice->precise)) {$osgood = true;}if (preg_match("/^[0-9]{1,3}=/", $content, $ndd)) {$nameddegrees++;} else {if ($osgood) {list($contentleft, $contentright) = array_merge(preg_split('/[|]/', $content), array(' '));$contents = questionnaire_choice_values($contentleft);if ($contents->title) {$contentleft = $contents->title;}$contents = questionnaire_choice_values($contentright);if ($contents->title) {$contentright = $contents->title;}$modality = strip_tags($contentleft.'|'.$contentright);$modality = preg_replace("/[\r\n\t]/", ' ', $modality);} else {$contents = questionnaire_choice_values($content);if ($contents->modname) {$modality = $contents->modname;} else if ($contents->title) {$modality = $contents->title;} else {$modality = strip_tags($contents->text);$modality = preg_replace("/[\r\n\t]/", ' ', $modality);}}$col = $choice->name.'->'.$modality;$columns[][$qpos] = $col;$questionidcols[][$qpos] = $qid.'_'.$choice->cid;array_push($types, $idtocsvmap[$type]);}}break;}} else {$columns[][$qpos] = $col;$questionidcols[][$qpos] = $qid;array_push($types, $idtocsvmap[$type]);}$num++;}array_push($output, $columns);$numrespcols = count($output[0]); // Number of columns used for storing question responses.// Flatten questionidcols.$tmparr = [];for ($c = 0; $c < $nbinfocols; $c++) {$tmparr[] = null; // Pad with non question columns.}foreach ($questionidcols as $i => $positions) {foreach ($positions as $position => $qid) {$tmparr[] = $qid;}}$questionidcols = $tmparr;// Create array of question positions hashed by question / question + choiceid.// And array of questions hashed by position.$questionpositions = [];$questionsbyposition = [];$p = 0;foreach ($questionidcols as $qid) {if ($qid === null) {// This is just padding, skip.$p++;continue;}$questionpositions[$qid] = $p;if (strpos($qid, '_') !== false) {$tmparr = explode ('_', $qid);$questionid = $tmparr[0];} else {$questionid = $qid;}$questionsbyposition[$p] = $this->questions[$questionid];$p++;}$formatoptions = new stdClass();$formatoptions->filter = false; // To prevent any filtering in CSV output.if ($rankaverages) {$averages = [];$rids = [];$allresponsesrs2 = $this->get_survey_all_responses($rid, $userid, $currentgroupid);foreach ($allresponsesrs2 as $responserow) {if (!isset($rids[$responserow->rid])) {$rids[$responserow->rid] = $responserow->rid;}}}// Get textual versions of responses, add them to output at the correct col position.$prevresprow = false; // Previous response row.$row = [];if ($rankaverages) {$averagerow = [];}foreach ($allresponsesrs as $responserow) {$rid = $responserow->rid;$qid = $responserow->question_id;// It's possible for a response to exist for a deleted question. Ignore these.if (!isset($this->questions[$qid])) {continue;}$question = $this->questions[$qid];$qtype = intval($question->type_id);if ($rankaverages) {if ($qtype === QUESRATE) {if (empty($averages[$qid])) {$results = $this->questions[$qid]->responsetype->get_results($rids);foreach ($results as $qresult) {$averages[$qid][$qresult->id] = $qresult->average;}}}}$questionobj = $this->questions[$qid];if ($prevresprow !== false && $prevresprow->rid !== $rid) {$output[] = $this->process_csv_row($row, $prevresprow, $currentgroupid, $questionsbyposition,$nbinfocols, $numrespcols, $showincompletes);$row = [];}if ($qtype === QUESRATE || $qtype === QUESCHECK) {$key = $qid.'_'.$responserow->choice_id;$position = $questionpositions[$key];if ($qtype === QUESRATE) {$choicetxt = $responserow->rankvalue;if ($rankaverages) {$averagerow[$position] = $averages[$qid][$responserow->choice_id];}} else {$content = $choicesbyqid[$qid][$responserow->choice_id]->content;if (\mod_questionnaire\question\choice::content_is_other_choice($content)) {// If this is an "other" column, put the text entered in the next position.$row[$position + 1] = $responserow->response;$choicetxt = empty($responserow->choice_id) ? '0' : '1';} else if (!empty($responserow->choice_id)) {$choicetxt = '1';} else {$choicetxt = '0';}}$responsetxt = $choicetxt;$row[$position] = $responsetxt;} else {$position = $questionpositions[$qid];if ($questionobj->has_choices()) {// This is choice type question, so process as so.$c = 0;if (in_array(intval($question->type_id), $choicetypes)) {$choices = $choicesbyqid[$qid];// Get position of choice.foreach ($choices as $choice) {$c++;if ($responserow->choice_id === $choice->cid) {break;}}}$content = $choicesbyqid[$qid][$responserow->choice_id]->content;if (\mod_questionnaire\question\choice::content_is_other_choice($content)) {// If this has an "other" text, use it.$responsetxt = \mod_questionnaire\question\choice::content_other_choice_display($content);$responsetxt1 = $responserow->response;} else if (($choicecodes == 1) && ($choicetext == 1)) {$responsetxt = $c.' : '.$content;} else if ($choicecodes == 1) {$responsetxt = $c;} else {$responsetxt = $content;}} else if (intval($qtype) === QUESYESNO) {// At this point, the boolean responses are returned as characters in the "response"// field instead of "choice_id" for csv exports (CONTRIB-6436).$responsetxt = $responserow->response === 'y' ? "1" : "0";} else {// Strip potential html tags from modality name.$responsetxt = $responserow->response;if (!empty($responsetxt)) {$responsetxt = $responserow->response;$responsetxt = strip_tags($responsetxt);$responsetxt = preg_replace("/[\r\n\t]/", ' ', $responsetxt);}}$row[$position] = $responsetxt;// Check for "other" text and set it to the next position if present.if (!empty($responsetxt1)) {$responsetxt1 = preg_replace("/[\r\n\t]/", ' ', $responsetxt1);$row[$position + 1] = $responsetxt1;unset($responsetxt1);}}$prevresprow = $responserow;}if ($prevresprow !== false) {// Add final row to output. May not exist if no response data was ever present.$output[] = $this->process_csv_row($row, $prevresprow, $currentgroupid, $questionsbyposition,$nbinfocols, $numrespcols, $showincompletes);}// Add averages row if appropriate.if ($rankaverages) {$summaryrow = [];$summaryrow[0] = get_string('averagesrow', 'questionnaire');$i = 1;for ($i = 1; $i < $nbinfocols; $i++) {$summaryrow[$i] = '';}$pos = 0;for ($i = $nbinfocols; $i < $numrespcols; $i++) {$summaryrow[$i] = isset($averagerow[$i]) ? $averagerow[$i] : '';}$output[] = $summaryrow;}// Change table headers to incorporate actual question numbers.$numquestion = 0;$oldkey = 0;for ($i = $nbinfocols; $i < $numrespcols; $i++) {$sep = '';$thisoutput = current($output[0][$i]);$thiskey = key($output[0][$i]);// Case of unnamed rate single possible answer (full stop char is used for support).if (strstr($thisoutput, '->.')) {$thisoutput = str_replace('->.', '', $thisoutput);}// If variable is not named no separator needed between Question number and potential sub-variables.if ($thisoutput == '' || strstr($thisoutput, '->.') || substr($thisoutput, 0, 2) == '->'|| substr($thisoutput, 0, 1) == '_') {$sep = '';} else {$sep = '_';}if ($thiskey > $oldkey) {$oldkey = $thiskey;$numquestion++;}// Abbreviated modality name in multiple or rate questions (COLORS->blue=the color of the sky...).$pos = strpos($thisoutput, '=');if ($pos) {$thisoutput = substr($thisoutput, 0, $pos);}$out = 'Q'.sprintf("%02d", $numquestion).$sep.$thisoutput;$output[0][$i] = $out;}return $output;}/*** Function to move a question to a new position.* Adapted from feedback plugin.** @param int $moveqid The id of the question to be moved.* @param int $movetopos The position to move question to.**/public function move_question($moveqid, $movetopos) {global $DB;$questions = $this->questions;$movequestion = $this->questions[$moveqid];if (is_array($questions)) {$index = 1;foreach ($questions as $question) {if ($index == $movetopos) {$index++;}if ($question->id == $movequestion->id) {$movequestion->position = $movetopos;$DB->update_record("questionnaire_question", $movequestion);continue;}$question->position = $index;$DB->update_record("questionnaire_question", $question);$index++;}return true;}return false;}/*** Render the response analysis page.* @param int $rid* @param array $resps* @param bool $compare* @param bool $isgroupmember* @param bool $allresponses* @param int $currentgroupid* @param array $filteredsections* @return array|string*/public function response_analysis($rid, $resps, $compare, $isgroupmember, $allresponses, $currentgroupid,$filteredsections = null) {global $DB, $CFG;require_once($CFG->libdir.'/tablelib.php');require_once($CFG->dirroot.'/mod/questionnaire/drawchart.php');// Find if there are any feedbacks in this questionnaire.$sql = "SELECT * FROM {questionnaire_fb_sections} WHERE surveyid = ? AND section IS NOT NULL";if (!$fbsections = $DB->get_records_sql($sql, [$this->survey->id])) {return '';}$action = optional_param('action', 'vall', PARAM_ALPHA);$resp = $DB->get_record('questionnaire_response', ['id' => $rid]);if (!empty($resp)) {$userid = $resp->userid;$user = $DB->get_record('user', ['id' => $userid]);if (!empty($user)) {if ($this->respondenttype == 'anonymous') {$ruser = '- ' . get_string('anonymous', 'questionnaire') . ' -';} else {$ruser = fullname($user);}}}// Available group modes (0 = no groups; 1 = separate groups; 2 = visible groups).$groupmode = groups_get_activity_groupmode($this->cm, $this->course);$groupname = get_string('allparticipants');if ($groupmode > 0) {if ($currentgroupid > 0) {$groupname = groups_get_group_name($currentgroupid);} else {$groupname = get_string('allparticipants');}}if ($this->survey->feedbackscores) {$table = new html_table();$table->size = [null, null];$table->align = ['left', 'right', 'right'];$table->head = [];$table->wrap = [];if ($compare) {$table->head = [get_string('feedbacksection', 'questionnaire'), $ruser, $groupname];} else {$table->head = [get_string('feedbacksection', 'questionnaire'), $groupname];}}$fbsectionsnb = array_keys($fbsections);$numsections = count($fbsections);// Get all response ids for all respondents.$rids = array();foreach ($resps as $key => $resp) {$rids[] = $key;}$nbparticipants = count($rids);$responsescores = [];// Calculate max score per question in questionnaire.$qmax = [];$maxtotalscore = 0;foreach ($this->questions as $question) {$qid = $question->id;if ($question->valid_feedback()) {$qmax[$qid] = $question->get_feedback_maxscore();$maxtotalscore += $qmax[$qid];// Get all the feedback scores for this question.$responsescores[$qid] = $question->get_feedback_scores($rids);}}// Just in case no values have been entered in the various questions possible answers field.if ($maxtotalscore === 0) {return '';}$feedbackmessages = [];// Get individual scores for each question in this responses set.$qscore = [];$allqscore = [];if (!$allresponses && $groupmode != 0) {$nbparticipants = max(1, $nbparticipants - !$isgroupmember);}foreach ($responsescores as $qid => $responsescore) {if (!empty($responsescore)) {foreach ($responsescore as $rrid => $response) {// If this is current user's response OR if current user is viewing another group's results.if ($rrid == $rid || $allresponses) {if (!isset($qscore[$qid])) {$qscore[$qid] = 0;}$qscore[$qid] = $response->score;}// Course score.if (!isset($allqscore[$qid])) {$allqscore[$qid] = 0;}// Only add current score if conditions below are met.if ($groupmode == 0 || $isgroupmember || (!$isgroupmember && $rrid != $rid) || $allresponses) {$allqscore[$qid] += $response->score;}}}}$totalscore = array_sum($qscore);$scorepercent = round($totalscore / $maxtotalscore * 100);$oppositescorepercent = 100 - $scorepercent;$alltotalscore = array_sum($allqscore);$allscorepercent = round($alltotalscore / $nbparticipants / $maxtotalscore * 100);// No need to go further if feedback is global, i.e. only relying on total score.if ($this->survey->feedbacksections == 1) {$sectionid = $fbsectionsnb[0];$sectionlabel = $fbsections[$sectionid]->sectionlabel;$sectionheading = $fbsections[$sectionid]->sectionheading;$labels = array();if ($feedbacks = $DB->get_records('questionnaire_feedback', ['sectionid' => $sectionid])) {foreach ($feedbacks as $feedback) {if ($feedback->feedbacklabel != '') {$labels[] = $feedback->feedbacklabel;}}}$feedback = $DB->get_record_select('questionnaire_feedback','sectionid = ? AND minscore <= ? AND ? < maxscore', [$sectionid, $scorepercent, $scorepercent]);// To eliminate all potential % chars in heading text (might interfere with the sprintf function).$sectionheading = str_replace('%', '', $sectionheading);// Replace section heading placeholders with their actual value (if any).$original = array('$scorepercent', '$oppositescorepercent');$result = array('%s%%', '%s%%');$sectionheading = str_replace($original, $result, $sectionheading);$sectionheading = sprintf($sectionheading , $scorepercent, $oppositescorepercent);$sectionheading = file_rewrite_pluginfile_urls($sectionheading, 'pluginfile.php',$this->context->id, 'mod_questionnaire', 'sectionheading', $sectionid);$feedbackmessages[] = $this->renderer->box_start();$feedbackmessages[] = format_text($sectionheading, FORMAT_HTML, ['noclean' => true]);$feedbackmessages[] = $this->renderer->box_end();if (!empty($feedback->feedbacktext)) {// Clean the text, ready for display.$formatoptions = new stdClass();$formatoptions->noclean = true;$feedbacktext = file_rewrite_pluginfile_urls($feedback->feedbacktext, 'pluginfile.php',$this->context->id, 'mod_questionnaire', 'feedback', $feedback->id);$feedbacktext = format_text($feedbacktext, $feedback->feedbacktextformat, $formatoptions);$feedbackmessages[] = $this->renderer->box_start();$feedbackmessages[] = $feedbacktext;$feedbackmessages[] = $this->renderer->box_end();}$score = array($scorepercent, 100 - $scorepercent);$allscore = null;if ($compare || $allresponses) {$allscore = array($allscorepercent, 100 - $allscorepercent);}$usergraph = get_config('questionnaire', 'usergraph');if ($usergraph && $this->survey->chart_type) {$this->page->add_to_page('feedbackcharts',draw_chart ($feedbacktype = 'global', $labels, $groupname,$allresponses, $this->survey->chart_type, $score, $allscore, $sectionlabel));}// Display class or group score. Pending chart library decision to display?// Find out if this feedback sectionlabel has a pipe separator.$lb = explode("|", $sectionlabel);$oppositescore = '';$oppositeallscore = '';if (count($lb) > 1) {$sectionlabel = $lb[0].' | '.$lb[1];$oppositescore = ' | '.$score[1].'%';$oppositeallscore = ' | '.$allscore[1].'%';}if ($this->survey->feedbackscores) {$table = $table ?? new html_table();if ($compare) {$table->data[] = array($sectionlabel, $score[0].'%'.$oppositescore, $allscore[0].'%'.$oppositeallscore);} else {$table->data[] = array($sectionlabel, $allscore[0].'%'.$oppositeallscore);}$this->page->add_to_page('feedbackscores', html_writer::table($table));}return $feedbackmessages;}// Now process scores for more than one section.// Initialize scores and maxscores to 0.$score = array();$allscore = array();$maxscore = array();$scorepercent = array();$allscorepercent = array();$oppositescorepercent = array();$alloppositescorepercent = array();$chartlabels = array();// Sections where all questions are unseen because of the $advdependencies.$nanscores = array();for ($i = 1; $i <= $numsections; $i++) {$score[$i] = 0;$allscore[$i] = 0;$maxscore[$i] = 0;$scorepercent[$i] = 0;}for ($section = 1; $section <= $numsections; $section++) {// Get feedback messages only for this sections.if (($filteredsections != null) && !in_array($section, $filteredsections)) {continue;}foreach ($fbsections as $key => $fbsection) {if ($fbsection->section == $section) {$feedbacksectionid = $key;$scorecalculation = section::decode_scorecalculation($fbsection->scorecalculation);if (empty($scorecalculation) && !is_array($scorecalculation)) {$scorecalculation = [];}$sectionheading = $fbsection->sectionheading;$imageid = $fbsection->id;$chartlabels[$section] = $fbsection->sectionlabel;}}foreach ($scorecalculation as $qid => $key) {// Just in case a question pertaining to a section has been deleted or made not required// after being included in scorecalculation.if (isset($qscore[$qid])) {$key = ($key == 0) ? 1 : $key;$score[$section] += round($qscore[$qid] * $key);$maxscore[$section] += round($qmax[$qid] * $key);if ($compare || $allresponses) {$allscore[$section] += round($allqscore[$qid] * $key);}}}if ($maxscore[$section] == 0) {array_push($nanscores, $section);}$scorepercent[$section] = ($maxscore[$section] > 0) ? (round($score[$section] / $maxscore[$section] * 100)) : 0;$oppositescorepercent[$section] = 100 - $scorepercent[$section];if (($compare || $allresponses) && $nbparticipants != 0) {$allscorepercent[$section] = ($maxscore[$section] > 0) ? (round(($allscore[$section] / $nbparticipants) /$maxscore[$section] * 100)) : 0;$alloppositescorepercent[$section] = 100 - $allscorepercent[$section];}if (!$allresponses) {if (is_nan($scorepercent[$section])) {// Info: all questions of $section are unseen// -> $scorepercent[$section] = round($score[$section] / $maxscore[$section] * 100) == NAN// -> $maxscore[$section] == 0 -> division by zero// $DB->get_record_select(...) fails, don't show feedbackmessage.continue;}// To eliminate all potential % chars in heading text (might interfere with the sprintf function).$sectionheading = str_replace('%', '', $sectionheading);// Replace section heading placeholders with their actual value (if any).$original = array('$scorepercent', '$oppositescorepercent');$result = array("$scorepercent[$section]%", "$oppositescorepercent[$section]%");$sectionheading = str_replace($original, $result, $sectionheading);$formatoptions = new stdClass();$formatoptions->noclean = true;$sectionheading = file_rewrite_pluginfile_urls($sectionheading, 'pluginfile.php',$this->context->id, 'mod_questionnaire', 'sectionheading', $imageid);$sectionheading = format_text($sectionheading, 1, $formatoptions);$feedbackmessages[] = $this->renderer->box_start('reportQuestionTitle');$feedbackmessages[] = format_text($sectionheading, FORMAT_HTML, $formatoptions);$feedback = $DB->get_record_select('questionnaire_feedback','sectionid = ? AND minscore <= ? AND ? < maxscore',array($feedbacksectionid, $scorepercent[$section], $scorepercent[$section]),'id,feedbacktext,feedbacktextformat');$feedbackmessages[] = $this->renderer->box_end();if (!empty($feedback->feedbacktext)) {// Clean the text, ready for display.$formatoptions = new stdClass();$formatoptions->noclean = true;$feedbacktext = file_rewrite_pluginfile_urls($feedback->feedbacktext, 'pluginfile.php',$this->context->id, 'mod_questionnaire', 'feedback', $feedback->id);$feedbacktext = format_text($feedbacktext, $feedback->feedbacktextformat, $formatoptions);$feedbackmessages[] = $this->renderer->box_start('feedbacktext');$feedbackmessages[] = $feedbacktext;$feedbackmessages[] = $this->renderer->box_end();}}}// Display class or group score.switch ($action) {case 'vallasort':asort($allscore);break;case 'vallarsort':arsort($allscore);break;default:}if ($this->survey->feedbackscores) {foreach ($allscore as $key => $sc) {if (isset($chartlabels[$key])) {$lb = explode("|", $chartlabels[$key]);$oppositescore = '';$oppositeallscore = '';if (count($lb) > 1) {$sectionlabel = $lb[0] . ' | ' . $lb[1];$oppositescore = ' | ' . $oppositescorepercent[$key] . '%';$oppositeallscore = ' | ' . $alloppositescorepercent[$key] . '%';} else {$sectionlabel = $chartlabels[$key];}// If all questions of $section are unseen then don't show feedbackscores for this section.if ($compare && !is_nan($scorepercent[$key])) {$table = $table ?? new html_table();$table->data[] = array($sectionlabel, $scorepercent[$key] . '%' . $oppositescore,$allscorepercent[$key] . '%' . $oppositeallscore);} else if (isset($allscorepercent[$key]) && !is_nan($allscorepercent[$key])) {$table = $table ?? new html_table();$table->data[] = array($sectionlabel, $allscorepercent[$key] . '%' . $oppositeallscore);}}}}$usergraph = get_config('questionnaire', 'usergraph');// Don't show feedbackcharts for sections in $nanscores -> remove sections from array.foreach ($nanscores as $val) {unset($chartlabels[$val]);unset($scorepercent[$val]);unset($allscorepercent[$val]);}if ($usergraph && $this->survey->chart_type) {$this->page->add_to_page('feedbackcharts',draw_chart('sections',array_values($chartlabels),$groupname,$allresponses,$this->survey->chart_type,array_values($scorepercent),array_values($allscorepercent),$sectionlabel));}if ($this->survey->feedbackscores) {$this->page->add_to_page('feedbackscores', html_writer::table($table));}return $feedbackmessages;}// Mobile support area./*** Save the data from the mobile app.* @param int $userid* @param int $sec* @param bool $completed* @param int $rid* @param bool $submit* @param string $action* @param array $responses* @return array*/public function save_mobile_data($userid, $sec, $completed, $rid, $submit, $action, array $responses) {global $DB, $CFG; // Do not delete "$CFG".$ret = [];$response = $this->build_response_from_appdata((object)$responses, $sec);$response->sec = $sec;$response->rid = $rid;$response->id = $rid;if ($action == 'nextpage') {$result = $this->next_page_action($response, $userid);if (is_string($result)) {$ret['warnings'] = $result;} else {$ret['nextpagenum'] = $result;}} else if ($action == 'previouspage') {$ret['nextpagenum'] = $this->previous_page_action($response, $userid);} else if (!$completed) {// If reviewing a completed questionnaire, don't insert a response.$msg = $this->response_check_format($response->sec, $response);if (empty($msg)) {$rid = $this->response_insert($response, $userid);} else {$ret['warnings'] = $msg;$ret['response'] = $response;}}if ($submit && (!isset($ret['warnings']) || empty($ret['warnings']))) {$this->commit_submission_response($rid, $userid);}return $ret;}/*** Get all of the areas that can have files.* @return array* @throws dml_exception*/public function get_all_file_areas() {global $DB;$areas = [];$areas['info'] = $this->sid;$areas['thankbody'] = $this->sid;// Add question areas.if (empty($this->questions)) {$this->add_questions();}$areas['question'] = [];foreach ($this->questions as $question) {$areas['question'][] = $question->id;}// Add feedback areas.$areas['feedbacknotes'] = $this->sid;$fbsections = $DB->get_records('questionnaire_fb_sections', ['surveyid' => $this->sid]);if (!empty($fbsections)) {$areas['sectionheading'] = [];foreach ($fbsections as $section) {$areas['sectionheading'][] = $section->id;$feedbacks = $DB->get_records('questionnaire_feedback', ['sectionid' => $section->id]);if (!empty($feedbacks)) {$areas['feedback'] = [];foreach ($feedbacks as $feedback) {$areas['feedback'][] = $feedback->id;}}}}return $areas;}}