Proyectos de Subversion Moodle

Rev

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 = '&nbsp;'.get_string('submitted', 'questionnaire').'&nbsp;'.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) . '&nbsp;';
            }
            $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 = '&nbsp;'.get_string('printblank', 'questionnaire');
            $title = get_string('printblanktooltip', 'questionnaire');
            $url = '/mod/questionnaire/print.php?qid='.$this->id.'&amp;rid=0&amp;'.'courseid='.$this->course->id.'&amp;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).'&nbsp;&nbsp;', '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&amp;sid='.$this->survey->id.
            '&amp;rid='.$rid.'&amp;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 = '&nbsp;'.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.'&amp;rid='.
                        $rids[$i].'&amp;individualresponse=1" >'.$ridsuserfullname[$i].'</a> ';
                } else {
                    $responsedate = '';
                    $resparr[] = '<a title = "'.$responsedate.'" href="'.$url.'&amp;rid='.
                        $rids[$i].'&amp;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.content
                  FROM {questionnaire_question} q
                  JOIN {questionnaire_quest_choice} c ON question_id = q.id
                 WHERE 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;
    }
}