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/>.

namespace mod_questionnaire\question;
use mod_questionnaire\edit_question_form;
use mod_questionnaire\responsetype\response\response;
use \questionnaire;

defined('MOODLE_INTERNAL') || die();
use \html_writer;

/**
 * This file contains the parent class for questionnaire question types.
 *
 * @author Mike Churchward
 * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org)
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
 * @package mod_questionnaire
 */
 // Constants.
define('QUESCHOOSE', 0);
define('QUESYESNO', 1);
define('QUESTEXT', 2);
define('QUESESSAY', 3);
define('QUESRADIO', 4);
define('QUESCHECK', 5);
define('QUESDROP', 6);
define('QUESRATE', 8);
define('QUESDATE', 9);
define('QUESNUMERIC', 10);
define('QUESSLIDER', 11);
define('QUESPAGEBREAK', 99);
define('QUESSECTIONTEXT', 100);

global $idcounter, $CFG;
$idcounter = 0;

require_once($CFG->dirroot.'/mod/questionnaire/locallib.php');


/**
 * Class for describing a question
 *
 * @author Mike Churchward
 * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org)
 * @package mod_questionnaire
 */
abstract class question {

    // Class Properties.
    /** @var int $id The database id of this question. */
    public $id = 0;

    /** @var int $surveyid The database id of the survey this question belongs to. */
    public $surveyid = 0;

    /** @var string $name The name of this question. */
    public $name = '';

    /** @var string $type The name of the question type. */
    public $type = '';

    /** @var array $choices Array holding any choices for this question. */
    public $choices = [];

    /** @var array $dependencies Array holding any dependencies for this question. */
    public $dependencies = [];

    /** @var string $responsetable The table name for responses. */
    public $responsetable = '';

    /** @var int $length The length field. */
    public $length = 0;

    /** @var int $precise The precision field. */
    public $precise = 0;

    /** @var int $position Position in the questionnaire */
    public $position = 0;

    /** @var string $content The question's content. */
    public $content = '';

    /** @var string $allchoices The list of all question's choices. */
    public $allchoices = '';

    /** @var boolean $required The required flag. */
    public $required = 'n';

    /** @var boolean $deleted The deleted flag. */
    public $deleted = 'n';

    /** @var mixed $extradata Any custom data for the question type. */
    public $extradata = '';

    /** @var array $qtypenames List of all question names. */
    private static $qtypenames = [
        QUESYESNO => 'yesno',
        QUESTEXT => 'text',
        QUESESSAY => 'essay',
        QUESRADIO => 'radio',
        QUESCHECK => 'check',
        QUESDROP => 'drop',
        QUESRATE => 'rate',
        QUESDATE => 'date',
        QUESNUMERIC => 'numerical',
        QUESPAGEBREAK => 'pagebreak',
        QUESSECTIONTEXT => 'sectiontext',
        QUESSLIDER => 'slider',
    ];

    /** @var array $notifications Array of extra messages for display purposes. */
    private $notifications = [];

    // Class Methods.

    /**
     * The class constructor
     * @param int $id
     * @param \stdClass $question
     * @param \context $context
     * @param array $params
     */
    public function __construct($id = 0, $question = null, $context = null, $params = []) {
        global $DB;
        static $qtypes = null;

        if ($qtypes === null) {
            $qtypes = $DB->get_records('questionnaire_question_type', [], 'typeid',
                                       'typeid, type, has_choices, response_table') ?? [];
        }

        if ($id) {
            $question = $DB->get_record('questionnaire_question', ['id' => $id]);
        }

        if (is_object($question)) {
            $this->id = $question->id;
            $this->surveyid = $question->surveyid;
            $this->name = $question->name;
            $this->length = $question->length;
            $this->precise = $question->precise;
            $this->position = $question->position;
            $this->content = $question->content;
            $this->required = $question->required;
            $this->deleted = $question->deleted;
            $this->extradata = $question->extradata;

            $this->type_id = $question->type_id;
            $this->type = $qtypes[$this->type_id]->type;
            $this->responsetable = $qtypes[$this->type_id]->response_table;

            if (!empty($question->choices)) {
                $this->choices = $question->choices;
            } else if ($qtypes[$this->type_id]->has_choices == 'y') {
                $this->get_choices();
            }
            // Added for dependencies.
            $this->get_dependencies();
        }
        $this->context = $context;

        foreach ($params as $property => $value) {
            $this->$property = $value;
        }

        if ($respclass = $this->responseclass()) {
            $this->responsetype = new $respclass($this);
        }
    }

    /**
     * Short name for this question type - no spaces, etc..
     * @return string
     */
    abstract public function helpname();

    /**
     * Build a question from data.
     * @param int $qtype
     * @param int|array $qdata
     * @param \stdClass $context
     * @return mixed
     */
    public static function question_builder($qtype, $qdata = null, $context = null) {
        $qclassname = '\\mod_questionnaire\\question\\'.self::qtypename($qtype);
        $qid = 0;
        if (!empty($qdata) && is_array($qdata)) {
            $qdata = (object)$qdata;
        } else if (!empty($qdata) && is_int($qdata)) {
            $qid = $qdata;
        }
        return new $qclassname($qid, $qdata, $context, ['type_id' => $qtype]);
    }

    /**
     * Return the different question type names.
     * @param int $qtype
     * @return string
     */
    public static function qtypename($qtype) {
        if (array_key_exists($qtype, self::$qtypenames)) {
            return self::$qtypenames[$qtype];
        } else {
            return('');
        }
    }

    /**
     * Return all of the different question type names.
     * @return array
     */
    public static function qtypenames() {
        return self::$qtypenames;
    }

    /**
     * Override and return true if the question has choices.
     * @return bool
     */
    public function has_choices() {
        return false;
    }

    /**
     * Load any choices into the object.
     * @throws \dml_exception
     */
    private function get_choices() {
        global $DB;

        if ($choices = $DB->get_records('questionnaire_quest_choice', ['question_id' => $this->id], 'id ASC')) {
            foreach ($choices as $choice) {
                $this->choices[$choice->id] = \mod_questionnaire\question\choice::create_from_data($choice);
            }
        } else {
            $this->choices = [];
        }
    }

    /**
     * Return true if this question has been marked as required.
     * @return bool
     */
    public function required() {
        return ($this->required == 'y');
    }

    /**
     * Return true if the question has defined dependencies.
     * @return bool
     */
    public function has_dependencies() {
        return !empty($this->dependencies);
    }

    /**
     * Override this and return true if the question type allows dependent questions.
     * @return bool
     */
    public function allows_dependents() {
        return false;
    }

    /**
     * Load any dependencies.
     */
    private function get_dependencies() {
        global $DB;

        $this->dependencies = [];
        if ($dependencies = $DB->get_records('questionnaire_dependency',
            ['questionid' => $this->id , 'surveyid' => $this->surveyid], 'id ASC')) {
            foreach ($dependencies as $dependency) {
                $this->dependencies[$dependency->id] = new \stdClass();
                $this->dependencies[$dependency->id]->dependquestionid = $dependency->dependquestionid;
                $this->dependencies[$dependency->id]->dependchoiceid = $dependency->dependchoiceid;
                $this->dependencies[$dependency->id]->dependlogic = $dependency->dependlogic;
                $this->dependencies[$dependency->id]->dependandor = $dependency->dependandor;
            }
        }
    }

    /**
     * Returns an array of dependency options for the question as an array of id value / display value pairs. Override in specific
     * question types that support this differently.
     * @return array An array of valid pair options.
     */
    protected function get_dependency_options() {
        $options = [];
        if ($this->allows_dependents() && $this->has_choices()) {
            foreach ($this->choices as $key => $choice) {
                $contents = questionnaire_choice_values($choice->content);
                if (!empty($contents->modname)) {
                    $choice->content = $contents->modname;
                } else if (!empty($contents->title)) { // Must be an image; use its title for the dropdown list.
                    $choice->content = format_string($contents->title);
                } else {
                    $choice->content = format_string($contents->text);
                }
                $options[$this->id . ',' . $key] = $this->name . '->' . $choice->content;
            }
        }
        return $options;
    }

    /**
     * Return true if all dependencies or this question have been fulfilled, or there aren't any.
     * @param int $rid The response ID to check.
     * @param array $questions An array containing all possible parent question objects.
     * @return bool
     */
    public function dependency_fulfilled($rid, $questions) {
        if (!$this->has_dependencies()) {
            $fulfilled = true;
        } else {
            foreach ($this->dependencies as $dependency) {
                $choicematches = $questions[$dependency->dependquestionid]->response_has_choice($rid, $dependency->dependchoiceid);

                // Note: dependencies are sorted, first all and-dependencies, then or-dependencies.
                if ($dependency->dependandor == 'and') {
                    $dependencyandfulfilled = false;
                    // This answer given.
                    if (($dependency->dependlogic == 1) && $choicematches) {
                        $dependencyandfulfilled = true;
                    }

                    // This answer NOT given.
                    if (($dependency->dependlogic == 0) && !$choicematches) {
                        $dependencyandfulfilled = true;
                    }

                    // Something mandatory not fulfilled? Stop looking and continue to next question.
                    if ($dependencyandfulfilled == false) {
                        break;
                    }

                    // In case we have no or-dependencies.
                    $dependencyorfulfilled = true;
                }

                // Note: dependencies are sorted, first all and-dependencies, then or-dependencies.
                if ($dependency->dependandor == 'or') {
                    $dependencyorfulfilled = false;
                    // To reach this point, the and-dependencies have all been fultilled or do not exist, so set them ok.
                    $dependencyandfulfilled = true;
                    // This answer given.
                    if (($dependency->dependlogic == 1) && $choicematches) {
                        $dependencyorfulfilled = true;
                    }

                    // This answer NOT given.
                    if (($dependency->dependlogic == 0) && !$choicematches) {
                        $dependencyorfulfilled = true;
                    }

                    // Something fulfilled? A single match is sufficient so continue to next question.
                    if ($dependencyorfulfilled == true) {
                        break;
                    }
                }

            }
            $fulfilled = ($dependencyandfulfilled && $dependencyorfulfilled);
        }
        return $fulfilled;
    }

    /**
     * Return the responsetype table for this question.
     * @return string
     */
    public function response_table() {
        return $this->responsetype->response_table();
    }

    /**
     * Return true if the specified response for this question contains the specified choice.
     * @param int $rid
     * @param int $choiceid
     * @return bool
     */
    public function response_has_choice($rid, $choiceid) {
        global $DB;
        $choiceval = $this->responsetype->transform_choiceid($choiceid);
        return $DB->record_exists($this->response_table(),
            ['response_id' => $rid, 'question_id' => $this->id, 'choice_id' => $choiceval]);
    }

    /**
     * Insert response data method.
     * @param \stdClass $responsedata All of the responsedata.
     * @return bool
     */
    public function insert_response($responsedata) {
        if (isset($this->responsetype) && is_object($this->responsetype) &&
            is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) {
            return $this->responsetype->insert_response($responsedata);
        } else {
            return false;
        }
    }

    /**
     * Get results data method.
     * @param array|bool $rids
     * @return array|false
     */
    public function get_results($rids = false) {
        if (isset ($this->responsetype) && is_object($this->responsetype) &&
            is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) {
            return $this->responsetype->get_results($rids);
        } else {
            return false;
        }
    }

    /**
     * Display results method.
     * @param bool $rids
     * @param string $sort
     * @param bool $anonymous
     * @return false|string
     */
    public function display_results($rids=false, $sort='', $anonymous=false) {
        if (isset ($this->responsetype) && is_object($this->responsetype) &&
            is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) {
            return $this->responsetype->display_results($rids, $sort, $anonymous);
        } else {
            return false;
        }
    }

    /**
     * Add a notification.
     * @param string $message
     */
    public function add_notification($message) {
        $this->notifications[] = $message;
    }

    /**
     * Get any notifications.
     * @return array | boolean The notifications array or false.
     */
    public function get_notifications() {
        if (empty($this->notifications)) {
            return false;
        } else {
            return $this->notifications;
        }
    }

    /**
     * Each question type must define its response class.
     * @return object The response object based off of questionnaire_response_base.
     */
    abstract protected function responseclass();

    /**
     * True if question type allows responses.
     * @return bool
     */
    public function supports_responses() {
        return !empty($this->responseclass());
    }

    /**
     * True if question type supports feedback options. False by default.
     * @return bool
     */
    public function supports_feedback() {
        return false;
    }

    /**
     * True if question type supports feedback scores and weights. Same as supports_feedback() by default.
     * @return bool
     */
    public function supports_feedback_scores() {
        return $this->supports_feedback();
    }

    /**
     * True if the question supports feedback and has valid settings for feedback. Override if the default logic is not enough.
     * @return bool
     */
    public function valid_feedback() {
        if ($this->supports_feedback() && $this->has_choices() && $this->required() && !empty($this->name)) {
            foreach ($this->choices as $choice) {
                if ($choice->value != null) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Provide the feedback scores for all requested response id's. This should be provided only by questions that provide feedback.
     * @param array $rids
     * @return array|bool
     */
    public function get_feedback_scores(array $rids) {
        if ($this->valid_feedback() && isset($this->responsetype) && is_object($this->responsetype) &&
            is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) {
            return $this->responsetype->get_feedback_scores($rids);
        } else {
            return false;
        }
    }

    /**
     * Get the maximum score possible for feedback if appropriate. Override if default behaviour is not correct.
     * @return int|bool
     */
    public function get_feedback_maxscore() {
        if ($this->valid_feedback()) {
            $maxscore = 0;
            foreach ($this->choices as $choice) {
                if (isset($choice->value) && ($choice->value != null)) {
                    if ($choice->value > $maxscore) {
                        $maxscore = $choice->value;
                    }
                }
            }
        } else {
            $maxscore = false;
        }
        return $maxscore;
    }

    /**
     * Check question's form data for complete response.
     * @param \stdClass $responsedata The data entered into the response.
     * @return bool
     */
    public function response_complete($responsedata) {
        if (is_a($responsedata, 'mod_questionnaire\responsetype\response\response')) {
            // If $responsedata is a response object, look through the answers.
            if (isset($responsedata->answers[$this->id]) && !empty($responsedata->answers[$this->id])) {
                $answer = $responsedata->answers[$this->id][0];
                if (!empty($answer->choiceid) && isset($this->choices[$answer->choiceid]) &&
                    $this->choices[$answer->choiceid]->is_other_choice()) {
                    $answered = !empty($answer->value);
                } else {
                    $answered = (!empty($answer->choiceid) || !empty($answer->value));
                }
            } else {
                $answered = false;
            }
        } else {
            // If $responsedata is webform data, check that its not empty.
            $answered = isset($responsedata->{'q'.$this->id}) && ($responsedata->{'q'.$this->id} != '');
        }
        return !($this->required() && ($this->deleted == 'n') && !$answered);
    }

    /**
     * Check question's form data for valid response. Override this if type has specific format requirements.
     * @param \stdClass $responsedata The data entered into the response.
     * @return bool
     */
    public function response_valid($responsedata) {
        return true;
    }

    /**
     * Update data record from object or optional question data.
     * @param \stdClass $questionrecord An object with all updated question record data.
     * @param bool $updatechoices True if choices should also be updated.
     */
    public function update($questionrecord = null, $updatechoices = true) {
        global $DB;

        if ($questionrecord === null) {
            $questionrecord = new \stdClass();
            $questionrecord->id = $this->id;
            $questionrecord->surveyid = $this->surveyid;
            $questionrecord->name = $this->name;
            $questionrecord->type_id = $this->type_id;
            $questionrecord->result_id = $this->result_id;
            $questionrecord->length = $this->length;
            $questionrecord->precise = $this->precise;
            $questionrecord->position = $this->position;
            $questionrecord->content = $this->content;
            $questionrecord->required = $this->required;
            $questionrecord->deleted = $this->deleted;
            $questionrecord->extradata = $this->extradata;
            $questionrecord->dependquestion = $this->dependquestion;
            $questionrecord->dependchoice = $this->dependchoice;
        } else {
            // Make sure the "id" field is this question's.
            if (isset($this->qid) && ($this->qid > 0)) {
                $questionrecord->id = $this->qid;
            } else {
                $questionrecord->id = $this->id;
            }
        }
        $DB->update_record('questionnaire_question', $questionrecord);

        if ($updatechoices && $this->has_choices()) {
            $this->update_choices();
        }
    }

    /**
     * Add the question to the database from supplied arguments.
     * @param \stdClass $questionrecord The required data for adding the question.
     * @param array $choicerecords An array of choice records with 'content' and 'value' properties.
     * @param boolean $calcposition Whether or not to calculate the next available position in the survey.
     */
    public function add($questionrecord, array $choicerecords = null, $calcposition = true) {
        global $DB;

        // Create new question.
        if ($calcposition) {
            // Set the position to the end.
            $sql = 'SELECT MAX(position) as maxpos '.
                   'FROM {questionnaire_question} '.
                   'WHERE surveyid = ? AND deleted = ?';
            $params = ['surveyid' => $questionrecord->surveyid, 'deleted' => 'n'];
            if ($record = $DB->get_record_sql($sql, $params)) {
                $questionrecord->position = $record->maxpos + 1;
            } else {
                $questionrecord->position = 1;
            }
        }

        // Make sure we add all necessary data.
        if (!isset($questionrecord->type_id) || empty($questionrecord->type_id)) {
            $questionrecord->type_id = $this->type_id;
        }

        $this->qid = $DB->insert_record('questionnaire_question', $questionrecord);

        if ($this->has_choices() && !empty($choicerecords)) {
            foreach ($choicerecords as $choicerecord) {
                $choicerecord->question_id = $this->qid;
                $this->add_choice($choicerecord);
            }
        }
    }

    /**
     * Update all choices.
     * @return bool
     */
    public function update_choices() {
        $retvalue = true;
        if ($this->has_choices() && isset($this->choices)) {
            // Need to fix this messed-up qid/id issue.
            if (isset($this->qid) && ($this->qid > 0)) {
                $qid = $this->qid;
            } else {
                $qid = $this->id;
            }
            foreach ($this->choices as $key => $choice) {
                $choicerecord = new \stdClass();
                $choicerecord->id = $key;
                $choicerecord->question_id = $qid;
                $choicerecord->content = $choice->content;
                $choicerecord->value = $choice->value;
                $retvalue &= $this->update_choice($choicerecord);
            }
        }
        return $retvalue;
    }

    /**
     * Update the choice with the choicerecord.
     * @param \stdClass $choicerecord
     * @return bool
     */
    public function update_choice($choicerecord) {
        global $DB;
        return $DB->update_record('questionnaire_quest_choice', $choicerecord);
    }

    /**
     * Add a new choice to the database.
     * @param \stdClass $choicerecord
     * @return bool
     */
    public function add_choice($choicerecord) {
        global $DB;
        $retvalue = true;
        if ($cid = $DB->insert_record('questionnaire_quest_choice', $choicerecord)) {
            $this->choices[$cid] = new \stdClass();
            $this->choices[$cid]->content = $choicerecord->content;
            $this->choices[$cid]->value = isset($choicerecord->value) ? $choicerecord->value : null;
        } else {
            $retvalue = false;
        }
        return $retvalue;
    }

    /**
     * Delete the choice from the question object and the database.
     * @param int|\stdClass $choice Either the integer id of the choice, or the choice record.
     */
    public function delete_choice($choice) {
        $retvalue = true;
        if (is_int($choice)) {
            $cid = $choice;
        } else {
            $cid = $choice->id;
        }
        if (\mod_questionnaire\question\choice::delete_from_db_by_id($cid)) {
            unset($this->choices[$cid]);
        } else {
            $retvalue = false;
        }
        return $retvalue;
    }

    /**
     * Insert extradata field into db. This will be stored as a string. If a question needs a different format, override this.
     * @param string $extradata
     * @return bool
     */
    public function insert_extradata($extradata) {
        global $DB;
        return $DB->set_field('questionnaire_question', 'extradata', $extradata, ['id' => $this->id]);
    }

    /**
     * Update the dependency record.
     * @param \stdClass $dependencyrecord
     * @return bool
     */
    public function update_dependency($dependencyrecord) {
        global $DB;
        return $DB->update_record('questionnaire_dependency', $dependencyrecord);
    }

    /**
     * Add a dependency record.
     * @param \stdClass $dependencyrecord
     * @return bool
     */
    public function add_dependency($dependencyrecord) {
        global $DB;

        $retvalue = true;
        if ($did = $DB->insert_record('questionnaire_dependency', $dependencyrecord)) {
            $this->dependencies[$did] = new \stdClass();
            $this->dependencies[$did]->dependquestionid = $dependencyrecord->dependquestionid;
            $this->dependencies[$did]->dependchoiceid = $dependencyrecord->dependchoiceid;
            $this->dependencies[$did]->dependlogic = $dependencyrecord->dependlogic;
            $this->dependencies[$did]->dependandor = $dependencyrecord->dependandor;
        } else {
            $retvalue = false;
        }
        return $retvalue;
    }

    /**
     * Delete the dependency from the question object and the database.
     * @param int|\stdClass $dependency Either the integer id of the dependency, or the dependency record.
     */
    public function delete_dependency($dependency) {
        global $DB;

        $retvalue = true;
        if (is_int($dependency)) {
            $did = $dependency;
        } else {
            $did = $dependency->id;
        }
        if ($DB->delete_records('questionnaire_dependency', ['id' => $did])) {
            unset($this->dependencies[$did]);
        } else {
            $retvalue = false;
        }
        return $retvalue;
    }

    /**
     * Set the question required field in the object and database.
     * @param bool $required Whether question should be required or not.
     */
    public function set_required($required) {
        global $DB;
        $rval = $required ? 'y' : 'n';
        // Need to fix this messed-up qid/id issue.
        if (isset($this->qid) && ($this->qid > 0)) {
            $qid = $this->qid;
        } else {
            $qid = $this->id;
        }
        $this->required = $rval;
        return $DB->set_field('questionnaire_question', 'required', $rval, ['id' => $qid]);
    }

    /**
     * Question specific display method.
     * @param \stdClass $formdata
     * @param array $descendantsdata
     * @param bool $blankquestionnaire
     *
     */
    abstract protected function question_survey_display($formdata, $descendantsdata, $blankquestionnaire);

    /**
     * Question specific response display method.
     * @param \stdClass $data
     *
     */
    abstract protected function response_survey_display($data);

    /**
     * Override and return a form template if provided. Output of question_survey_display is iterpreted based on this.
     * @return bool|string
     */
    public function question_template() {
        return false;
    }

    /**
     * Override and return a form template if provided. Output of response_survey_display is iterpreted based on this.
     * @return bool|string
     */
    public function response_template() {
        return false;
    }

    /**
     * Override and return a form template if provided. Output of results_output is iterpreted based on this.
     * @param bool $pdf
     * @return bool|string
     */
    public function results_template($pdf = false) {
        if (isset ($this->responsetype) && is_object($this->responsetype) &&
            is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) {
            return $this->responsetype->results_template($pdf);
        } else {
            return false;
        }
    }

    /**
     * Get the output for question renderers / templates.
     * @param \mod_questionnaire\responsetype\response\response $response
     * @param boolean $blankquestionnaire
     * @param array $dependants Array of all questions/choices depending on this question.
     * @param int $qnum
     * @return \stdClass
     */
    public function question_output($response, $blankquestionnaire, $dependants=[], $qnum='') {
        $pagetags = $this->questionstart_survey_display($qnum, $response);
        $pagetags->qformelement = $this->question_survey_display($response, $dependants, $blankquestionnaire);
        return $pagetags;
    }

    /**
     * Get the output for question renderers / templates.
     * @param \mod_questionnaire\responsetype\response\response $response
     * @param string $qnum
     * @return \stdClass
     */
    public function response_output($response, $qnum='') {
        $pagetags = $this->questionstart_survey_display($qnum, $response);
        $pagetags->qformelement = $this->response_survey_display($response);
        return $pagetags;
    }

    /**
     * Get the output for the start of the questions in a survey.
     * @param int $qnum
     * @param \mod_questionnaire\responsetype\response\response $response
     * @return \stdClass
     */
    public function questionstart_survey_display($qnum, $response=null) {
        global $OUTPUT, $SESSION, $questionnaire, $PAGE;

        $pagetags = new \stdClass();
        $currenttab = $SESSION->questionnaire->current_tab;
        $pagetype = $PAGE->pagetype;
        $skippedclass = '';
        // If no questions autonumbering.
        $nonumbering = false;
        if (!$questionnaire->questions_autonumbered()) {
            $qnum = '';
            $nonumbering = true;
        }

        // For now, check what the response type is until we've got it all refactored.
        if ($response instanceof \mod_questionnaire\responsetype\response\response) {
            $skippedquestion = !isset($response->answers[$this->id]);
        } else {
            $skippedquestion = !empty($response) && !isset($response->{'q'.$this->id});
        }

        // If we are on report page and this questionnaire has dependquestions and this question was skipped.
        if (($pagetype == 'mod-questionnaire-myreport' || $pagetype == 'mod-questionnaire-report') &&
            ($nonumbering == false) && !empty($this->dependencies) && $skippedquestion) {
            $skippedclass = ' unselected';
            $qnum = '<span class="'.$skippedclass.'">('.$qnum.')</span>';
        }
        // In preview mode, hide children questions that have not been answered.
        // In report mode, If questionnaire is set to no numbering,
        // also hide answers to questions that have not been answered.
        $displayclass = 'qn-container';
        if ($pagetype == 'mod-questionnaire-preview' || ($nonumbering &&
            ($currenttab == 'mybyresponse' || $currenttab == 'individualresp'))) {
            // This needs to be done to ensure all dependency data is loaded.
            // TODO - Perhaps this should be a function called by the questionnaire after it loads all questions?
            $questionnaire->load_parents($this);
            // Want this to come from the renderer, meaning we need $questionnaire.
            $pagetags->dependencylist = $questionnaire->renderer->get_dependency_html($this->id, $this->dependencies);
        }

        $pagetags->fieldset = (object)['id' => $this->id, 'class' => $displayclass];

        // Do not display the info box for the label question type.
        if ($this->type_id != QUESSECTIONTEXT) {
            if (!$nonumbering) {
                $pagetags->qnum = $qnum;
            }
            $required = '';
            if ($this->required()) {
                $required = html_writer::start_tag('div', ['class' => 'accesshide']);
                $required .= get_string('required', 'questionnaire');
                $required .= html_writer::end_tag('div');
                $required .= html_writer::empty_tag('img', ['class' => 'req', 'title' => get_string('required', 'questionnaire'),
                    'alt' => get_string('required', 'questionnaire'), 'src' => $OUTPUT->image_url('req')]);
            }
            $pagetags->required = $required; // Need to replace this with better renderer / template?
        }
        // If question text is "empty", i.e. 2 non-breaking spaces were inserted, empty it.
        if ($this->content == '<p>  </p>') {
            $this->content = '';
        }
        $pagetags->skippedclass = $skippedclass;
        if ($this->type_id == QUESNUMERIC || $this->type_id == QUESTEXT) {
            $pagetags->label = (object)['for' => self::qtypename($this->type_id) . $this->id];
        } else if ($this->type_id == QUESDROP) {
            $pagetags->label = (object)['for' => self::qtypename($this->type_id) . $this->name];
        } else if ($this->type_id == QUESESSAY) {
            $pagetags->label = (object)['for' => 'edit-q' . $this->id];
        }
        $options = ['noclean' => true, 'para' => false, 'filter' => true, 'context' => $this->context, 'overflowdiv' => true];
        $content = format_text(file_rewrite_pluginfile_urls($this->content, 'pluginfile.php',
            $this->context->id, 'mod_questionnaire', 'question', $this->id), FORMAT_HTML, $options);
        $pagetags->qcontent = $content;

        return $pagetags;
    }

    // This section contains functions for editing the specific question types.
    // There are required methods that must be implemented, and helper functions that can be used.

    // Required functions that can be overridden by the question type.

    /**
     * Override this, or any of the internal methods, to provide specific form data for editing the question type.
     * The structure of the elements here is the default layout for the question form.
     * @param edit_question_form $form The main moodleform object.
     * @param questionnaire $questionnaire The questionnaire being edited.
     * @return bool
     */
    public function edit_form(edit_question_form $form, questionnaire $questionnaire) {
        $mform =& $form->_form;
        $this->form_header($mform);
        $this->form_name($mform);
        $this->form_required($mform);
        $this->form_length($mform);
        $this->form_precise($mform);
        $this->form_question_text($mform, ($form->_customdata['modcontext'] ?? ''));

        if ($this->has_choices()) {
            // This is used only by the question editing form.
            $this->allchoices = $this->form_choices($mform);
        }

        $this->form_extradata($mform);

        // Added for advanced dependencies, parameter $editformobject is needed to use repeat_elements.
        if ($questionnaire->navigate > 0) {
            $this->form_dependencies($form, $questionnaire->questions);
        }

        // Exclude the save/cancel buttons from any collapsing sections.
        $mform->closeHeaderBefore('buttonar');

        // Hidden fields.
        $mform->addElement('hidden', 'id', 0);
        $mform->setType('id', PARAM_INT);
        $mform->addElement('hidden', 'qid', 0);
        $mform->setType('qid', PARAM_INT);
        $mform->addElement('hidden', 'sid', 0);
        $mform->setType('sid', PARAM_INT);
        $mform->addElement('hidden', 'type_id', $this->type_id);
        $mform->setType('type_id', PARAM_INT);
        $mform->addElement('hidden', 'action', 'question');
        $mform->setType('action', PARAM_ALPHA);

        // Buttons.
        $buttonarray[] = &$mform->createElement('submit', 'submitbutton', get_string('savechanges'));
        if (isset($this->qid)) {
            $buttonarray[] = &$mform->createElement('submit', 'makecopy', get_string('saveasnew', 'questionnaire'));
        }
        $buttonarray[] = &$mform->createElement('cancel');
        $mform->addGroup($buttonarray, 'buttonar', '', [' '], false);

        return true;
    }

    /**
     * Add the form header.
     * @param \MoodleQuickForm $mform
     * @param string $helpname
     */
    protected function form_header(\MoodleQuickForm $mform, $helpname = '') {
        // Display different messages for new question creation and existing question modification.
        if (isset($this->qid) && !empty($this->qid)) {
            $header = get_string('editquestion', 'questionnaire', questionnaire_get_type($this->type_id));
        } else {
            $header = get_string('addnewquestion', 'questionnaire', questionnaire_get_type($this->type_id));
        }
        if (empty($helpname)) {
            $helpname = $this->helpname();
        }

        $mform->addElement('header', 'questionhdredit', $header);
        $mform->addHelpButton('questionhdredit', $helpname, 'questionnaire');
    }

    /**
     * Add the form name field.
     * @param \MoodleQuickForm $mform
     * @return \MoodleQuickForm
     */
    protected function form_name(\MoodleQuickForm $mform) {
        $mform->addElement('text', 'name', get_string('optionalname', 'questionnaire'),
                        ['size' => '30', 'maxlength' => '30']);
        $mform->setType('name', PARAM_TEXT);
        $mform->addHelpButton('name', 'optionalname', 'questionnaire');
        return $mform;
    }

    /**
     * Add the form required field.
     * @param \MoodleQuickForm $mform
     * @return \MoodleQuickForm
     */
    protected function form_required(\MoodleQuickForm $mform) {
        $reqgroup = [];
        $reqgroup[] =& $mform->createElement('radio', 'required', '', get_string('yes'), 'y');
        $reqgroup[] =& $mform->createElement('radio', 'required', '', get_string('no'), 'n');
        $mform->addGroup($reqgroup, 'reqgroup', get_string('required', 'questionnaire'), ' ', false);
        $mform->addHelpButton('reqgroup', 'required', 'questionnaire');
        return $mform;
    }

    /**
     * Return the length form element.
     * @param \MoodleQuickForm $mform
     * @param string $helpname
     */
    protected function form_length(\MoodleQuickForm $mform, $helpname = '') {
        self::form_length_text($mform, $helpname);
    }

    /**
     * Return the precision form element.
     * @param \MoodleQuickForm $mform
     * @param string $helpname
     */
    protected function form_precise(\MoodleQuickForm $mform, $helpname = '') {
        self::form_precise_text($mform, $helpname);
    }

    /**
     * Determine form dependencies.
     * @param \MoodleQuickForm $form The moodle form to add elements to.
     * @param array $questions
     * @return bool
     */
    protected function form_dependencies($form, $questions) {
        // Create a new area for multiple dependencies.
        $mform = $form->_form;
        $position = ($this->position !== 0) ? $this->position : count($questions) + 1;
        $dependencies = [];
        $dependencies[''][0] = get_string('choosedots');
        foreach ($questions as $question) {
            if (($question->position < $position) && !empty($question->name) &&
                !empty($dependopts = $question->get_dependency_options())) {
                $dependencies[$question->name] = $dependopts;
            }
        }

        $children = [];
        if (isset($this->qid)) {
            // Use also for the delete dialogue later.
            foreach ($questions as $questionlistitem) {
                if ($questionlistitem->has_dependencies()) {
                    foreach ($questionlistitem->dependencies as $key => $outerdependencies) {
                        if ($outerdependencies->dependquestionid == $this->qid) {
                            $children[$key] = $outerdependencies;
                        }
                    }
                }
            }
        }

        if (count($dependencies) > 1) {
            $mform->addElement('header', 'dependencies_hdr', get_string('dependencies', 'questionnaire'));
            $mform->setExpanded('dependencies_hdr');
            $mform->closeHeaderBefore('qst_and_choices_hdr');

            $dependenciescountand = 0;
            $dependenciescountor = 0;

            foreach ($this->dependencies as $dependency) {
                if ($dependency->dependandor == "and") {
                    $dependenciescountand++;
                } else if ($dependency->dependandor == "or") {
                    $dependenciescountor++;
                }
            }

            /* I decided to allow changing dependencies of parent questions, because forcing the editor to remove dependencies
             * bottom up, starting at the lowest child question is a pain for large questionnaires.
             * So the following "if" becomes the default and the else-branch is completely commented.
             * TODO Since the best way to get the list of child questions is currently to click on delete (and choose not to
             * delete), one might consider to list the child questions in addition here.
             */

            // Area for "must"-criteria.
            $mform->addElement('static', 'mandatory', '',
                '<div class="dimmed_text">' . get_string('mandatory', 'questionnaire') . '</div>');
            $selectand = $mform->createElement('select', 'dependlogic_and', get_string('condition', 'questionnaire'),
                [get_string('answernotgiven', 'questionnaire'), get_string('answergiven', 'questionnaire')]);
            $selectand->setSelected('1');
            $groupitemsand = [];
            $groupitemsand[] =& $mform->createElement('selectgroups', 'dependquestions_and',
                get_string('parent', 'questionnaire'), $dependencies);
            $groupitemsand[] =& $selectand;
            $groupand = $mform->createElement('group', 'selectdependencies_and', get_string('dependquestion', 'questionnaire'),
                $groupitemsand, ' ', false);
            $form->repeat_elements([$groupand], $dependenciescountand + 1, [],
                'numdependencies_and', 'adddependencies_and', 2, null, true);

            // Area for "can"-criteria.
            $mform->addElement('static', 'optional', '',
                '<div class="dimmed_text">' . get_string('optional', 'questionnaire') . '</div>');
            $selector = $mform->createElement('select', 'dependlogic_or', get_string('condition', 'questionnaire'),
                [get_string('answernotgiven', 'questionnaire'), get_string('answergiven', 'questionnaire')]);
            $selector->setSelected('1');
            $groupitemsor = [];
            $groupitemsor[] =& $mform->createElement('selectgroups', 'dependquestions_or',
                get_string('parent', 'questionnaire'), $dependencies);
            $groupitemsor[] =& $selector;
            $groupor = $mform->createElement('group', 'selectdependencies_or', get_string('dependquestion', 'questionnaire'),
                $groupitemsor, ' ', false);
            $form->repeat_elements([$groupor], $dependenciescountor + 1, [], 'numdependencies_or',
                'adddependencies_or', 2, null, true);
        }
        return true;
    }

    /**
     * Return the question text element.
     * @param \MoodleQuickForm $mform
     * @param string $context
     * @return \MoodleQuickForm
     */
    protected function form_question_text(\MoodleQuickForm $mform, $context) {
        $editoroptions = ['maxfiles' => EDITOR_UNLIMITED_FILES, 'trusttext' => true, 'context' => $context];
        $mform->addElement('editor', 'content', get_string('text', 'questionnaire'), null, $editoroptions);
        $mform->setType('content', PARAM_RAW);
        $mform->addRule('content', null, 'required', null, 'client');
        return $mform;
    }

    /**
     * Add the choices to the form.
     * @param \MoodleQuickForm $mform
     * @return string
     */
    protected function form_choices(\MoodleQuickForm $mform) {
        if ($this->has_choices()) {
            $numchoices = count($this->choices);
            $allchoices = '';
            foreach ($this->choices as $choice) {
                if (!empty($allchoices)) {
                    $allchoices .= "\n";
                }
                $allchoices .= $choice->content;
            }

            $helpname = $this->helpname();

            $mform->addElement('html', '<div class="qoptcontainer">');
            $options = ['wrap' => 'virtual', 'class' => 'qopts'];
            $mform->addElement('textarea', 'allchoices', get_string('possibleanswers', 'questionnaire'), $options);
            $mform->setType('allchoices', PARAM_RAW);
            $mform->addRule('allchoices', null, 'required', null, 'client');
            $mform->addHelpButton('allchoices', $helpname, 'questionnaire');
            $mform->addElement('html', '</div>');
            $mform->addElement('hidden', 'num_choices', $numchoices);
            $mform->setType('num_choices', PARAM_INT);
        }
        return $allchoices;
    }

    /**
     * Override if the question uses the extradata field.
     * @param \MoodleQuickForm $mform
     * @param string $helpname
     * @return \MoodleQuickForm
     */
    protected function form_extradata(\MoodleQuickForm $mform, $helpname = '') {
        $mform->addElement('hidden', 'extradata');
        $mform->setType('extradata', PARAM_INT);
        return $mform;
    }

    // Helper functions for commonly used editing functions.

    /**
     * Add the length element as hidden.
     * @param \MoodleQuickForm $mform
     * @param int $value
     * @return \MoodleQuickForm
     */
    public static function form_length_hidden(\MoodleQuickForm $mform, $value = 0) {
        $mform->addElement('hidden', 'length', $value);
        $mform->setType('length', PARAM_INT);
        return $mform;
    }

    /**
     * Add the length element as text.
     * @param \MoodleQuickForm $mform
     * @param string $helpname
     * @param int $value
     * @return \MoodleQuickForm
     */
    public static function form_length_text(\MoodleQuickForm $mform, $helpname = '', $value = 0) {
        $mform->addElement('text', 'length', get_string($helpname, 'questionnaire'), ['size' => '1'], $value);
        $mform->setType('length', PARAM_INT);
        if (!empty($helpname)) {
            $mform->addHelpButton('length', $helpname, 'questionnaire');
        }
        return $mform;
    }

    /**
     * Add the precise element as hidden.
     * @param \MoodleQuickForm $mform
     * @param int $value
     * @return \MoodleQuickForm
     */
    public static function form_precise_hidden(\MoodleQuickForm $mform, $value = 0) {
        $mform->addElement('hidden', 'precise', $value);
        $mform->setType('precise', PARAM_INT);
        return $mform;
    }

    /**
     * Add the precise element as text.
     * @param \MoodleQuickForm $mform
     * @param string $helpname
     * @param int $value
     * @return \MoodleQuickForm
     * @throws \coding_exception
     */
    public static function form_precise_text(\MoodleQuickForm $mform, $helpname = '', $value = 0) {
        $mform->addElement('text', 'precise', get_string($helpname, 'questionnaire'), ['size' => '1']);
        $mform->setType('precise', PARAM_INT);
        if (!empty($helpname)) {
            $mform->addHelpButton('precise', $helpname, 'questionnaire');
        }
        return $mform;
    }

    /**
     * Create and update question data from the forms.
     * @param \stdClass $formdata
     * @param questionnaire $questionnaire
     */
    public function form_update($formdata, $questionnaire) {
        global $DB;

        $this->form_preprocess_data($formdata);
        if (!empty($formdata->qid)) {

            // Update existing question.
            // Handle any attachments in the content.
            $formdata->itemid = $formdata->content['itemid'];
            $formdata->format = $formdata->content['format'];
            $formdata->content = $formdata->content['text'];
            $formdata->content = file_save_draft_area_files($formdata->itemid, $questionnaire->context->id, 'mod_questionnaire',
                'question', $formdata->qid, ['subdirs' => true], $formdata->content);

            $fields = ['name', 'type_id', 'length', 'precise', 'required', 'content', 'extradata'];
            $questionrecord = new \stdClass();
            $questionrecord->id = $formdata->qid;
            foreach ($fields as $f) {
                if (isset($formdata->$f)) {
                    $questionrecord->$f = trim($formdata->$f);
                }
            }

            $this->update($questionrecord, false);

            if ($questionnaire->has_dependencies()) {
                questionnaire_check_page_breaks($questionnaire);
            }
        } else {
            // Create new question:
            // Need to update any image content after the question is created, so create then update the content.
            $formdata->surveyid = $formdata->sid;
            $fields = ['surveyid', 'name', 'type_id', 'length', 'precise', 'required', 'position', 'extradata'];
            $questionrecord = new \stdClass();
            foreach ($fields as $f) {
                if (isset($formdata->$f)) {
                    $questionrecord->$f = trim($formdata->$f);
                }
            }
            $questionrecord->content = '';

            $this->add($questionrecord);

            // Handle any attachments in the content.
            $formdata->itemid = $formdata->content['itemid'];
            $formdata->format = $formdata->content['format'];
            $formdata->content = $formdata->content['text'];
            $content = file_save_draft_area_files($formdata->itemid, $questionnaire->context->id, 'mod_questionnaire',
                'question', $this->qid, ['subdirs' => true], $formdata->content);
            $DB->set_field('questionnaire_question', 'content', $content, ['id' => $this->qid]);
        }

        if ($this->has_choices()) {
            // Now handle any choice updates.
            $cidx = 0;
            if (isset($this->choices) && !isset($formdata->makecopy)) {
                $oldcount = count($this->choices);
                $echoice = reset($this->choices);
                $ekey = key($this->choices);
            } else {
                $oldcount = 0;
            }

            $newchoices = explode("\n", $formdata->allchoices);
            $nidx = 0;
            $newcount = count($newchoices);

            while (($nidx < $newcount) && ($cidx < $oldcount)) {
                if ($newchoices[$nidx] != $echoice->content) {
                    $choicerecord = new \stdClass();
                    $choicerecord->id = $ekey;
                    $choicerecord->question_id = $this->qid;
                    $choicerecord->content = trim($newchoices[$nidx]);
                    $r = preg_match_all("/^(\d{1,2})(=.*)$/", $newchoices[$nidx], $matches);
                    // This choice has been attributed a "score value" OR this is a rate question type.
                    if ($r) {
                        $newscore = $matches[1][0];
                        $choicerecord->value = $newscore;
                    } else {     // No score value for this choice.
                        $choicerecord->value = null;
                    }
                    $this->update_choice($choicerecord);
                }
                $nidx++;
                $echoice = next($this->choices);
                $ekey = key($this->choices);
                $cidx++;
            }

            while ($nidx < $newcount) {
                // New choices.
                $choicerecord = new \stdClass();
                $choicerecord->question_id = $this->qid;
                $choicerecord->content = trim($newchoices[$nidx]);
                $r = preg_match_all("/^(\d{1,2})(=.*)$/", $choicerecord->content, $matches);
                // This choice has been attributed a "score value" OR this is a rate question type.
                if ($r) {
                    $choicerecord->value = $matches[1][0];
                }
                $this->add_choice($choicerecord);
                $nidx++;
            }

            while ($cidx < $oldcount) {
                end($this->choices);
                $ekey = key($this->choices);
                $this->delete_choice($ekey);
                $cidx++;
            }
        }

        // Now handle the dependencies the same way as choices.
        // Shouldn't the MOODLE-API provide this case of insert/update/delete?.
        // First handle dependendies updates.
        if (!isset($formdata->fixed_deps)) {
            if ($this->has_dependencies() && !isset($formdata->makecopy)) {
                $oldcount = count($this->dependencies);
                $edependency = reset($this->dependencies);
                $ekey = key($this->dependencies);
            } else {
                $oldcount = 0;
            }

            $cidx = 0;
            $nidx = 0;

            // All 3 arrays in this object have the same length.
            if (isset($formdata->dependquestion)) {
                $newcount = count($formdata->dependquestion);
            } else {
                $newcount = 0;
            }
            while (($nidx < $newcount) && ($cidx < $oldcount)) {
                if ($formdata->dependquestion[$nidx] != $edependency->dependquestionid ||
                    $formdata->dependchoice[$nidx] != $edependency->dependchoiceid ||
                    $formdata->dependlogic_cleaned[$nidx] != $edependency->dependlogic ||
                    $formdata->dependandor[$nidx] != $edependency->dependandor) {

                    $dependencyrecord = new \stdClass();
                    $dependencyrecord->id = $ekey;
                    $dependencyrecord->questionid = $this->qid;
                    $dependencyrecord->surveyid = $this->surveyid;
                    $dependencyrecord->dependquestionid = $formdata->dependquestion[$nidx];
                    $dependencyrecord->dependchoiceid = $formdata->dependchoice[$nidx];
                    $dependencyrecord->dependlogic = $formdata->dependlogic_cleaned[$nidx];
                    $dependencyrecord->dependandor = $formdata->dependandor[$nidx];

                    $this->update_dependency($dependencyrecord);
                }
                $nidx++;
                $edependency = next($this->dependencies);
                $ekey = key($this->dependencies);
                $cidx++;
            }

            while ($nidx < $newcount) {
                // New dependencies.
                $dependencyrecord = new \stdClass();
                $dependencyrecord->questionid = $this->qid;
                $dependencyrecord->surveyid = $formdata->sid;
                $dependencyrecord->dependquestionid = $formdata->dependquestion[$nidx];
                $dependencyrecord->dependchoiceid = $formdata->dependchoice[$nidx];
                $dependencyrecord->dependlogic = $formdata->dependlogic_cleaned[$nidx];
                $dependencyrecord->dependandor = $formdata->dependandor[$nidx];

                $this->add_dependency($dependencyrecord);
                $nidx++;
            }

            while ($cidx < $oldcount) {
                end($this->dependencies);
                $ekey = key($this->dependencies);
                $this->delete_dependency($ekey);
                $cidx++;
            }
        }
    }

    /**
     * Any preprocessing of general data.
     * @param \stdClass $formdata
     * @return bool
     */
    protected function form_preprocess_data($formdata) {
        if ($this->has_choices()) {
            // Eliminate trailing blank lines.
            $formdata->allchoices = preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $formdata->allchoices);
            // Trim to eliminate potential trailing carriage return.
            $formdata->allchoices = trim($formdata->allchoices);
            $this->form_preprocess_choicedata($formdata);
        }

        // Dependencies logic does not (yet) need preprocessing, might change with more complex conditions.
        // Check, if entries exist and whether they are not only 0 (form elements created but no value selected).
        if (isset($formdata->dependquestions_and) &&
            !(count(array_keys($formdata->dependquestions_and, 0, true)) == count($formdata->dependquestions_and))) {
            for ($i = 0; $i < count($formdata->dependquestions_and); $i++) {
                $dependency = explode(",", $formdata->dependquestions_and[$i]);

                if ($dependency[0] != 0) {
                    $formdata->dependquestion[] = $dependency[0];
                    $formdata->dependchoice[] = $dependency[1];
                    $formdata->dependlogic_cleaned[] = $formdata->dependlogic_and[$i];
                    $formdata->dependandor[] = "and";
                }
            }
        }

        if (isset($formdata->dependquestions_or) &&
            !(count(array_keys($formdata->dependquestions_or, 0, true)) == count($formdata->dependquestions_or))) {
            for ($i = 0; $i < count($formdata->dependquestions_or); $i++) {
                $dependency = explode(",", $formdata->dependquestions_or[$i]);

                if ($dependency[0] != 0) {
                    $formdata->dependquestion[] = $dependency[0];
                    $formdata->dependchoice[] = $dependency[1];
                    $formdata->dependlogic_cleaned[] = $formdata->dependlogic_or[$i];
                    $formdata->dependandor[] = "or";
                }
            }
        }
        return true;
    }

    /**
     * Override this function for question specific choice preprocessing.
     * @param \stdClass $formdata
     * @return false
     */
    protected function form_preprocess_choicedata($formdata) {
        if (empty($formdata->allchoices)) {
            error (get_string('enterpossibleanswers', 'questionnaire'));
        }
        return false;
    }

    /**
     * True if question provides mobile support.
     * @return bool
     */
    public function supports_mobile() {
        return false;
    }

    /**
     * Override and return false if not supporting mobile app.
     * @param int $qnum
     * @param bool $autonum
     * @return \stdClass
     */
    public function mobile_question_display($qnum, $autonum = false) {
        $options = ['noclean' => true, 'para' => false, 'filter' => true,
            'context' => $this->context, 'overflowdiv' => true];
        $mobiledata = (object)[
            'id' => $this->id,
            'name' => $this->name,
            'type_id' => $this->type_id,
            'length' => $this->length,
            'content' => format_text(file_rewrite_pluginfile_urls($this->content, 'pluginfile.php', $this->context->id,
                'mod_questionnaire', 'question', $this->id), FORMAT_HTML, $options),
            'content_stripped' => strip_tags($this->content),
            'required' => ($this->required == 'y') ? 1 : 0,
            'deleted' => $this->deleted,
            'response_table' => $this->responsetable,
            'fieldkey' => $this->mobile_fieldkey(),
            'precise' => $this->precise,
            'qnum' => $qnum,
            'errormessage' => get_string('required') . ': ' . $this->name
        ];
        $mobiledata->choices = $this->mobile_question_choices_display();

        if ($this->mobile_question_extradata_display()) {
            $mobiledata->extradata = json_decode($this->extradata);
        }
        if ($autonum) {
            $mobiledata->content = $qnum . '. ' . $mobiledata->content;
            $mobiledata->content_stripped = $qnum . '. ' . $mobiledata->content_stripped;
        }
        $mobiledata->responses = '';
        return $mobiledata;
    }

    /**
     * Override and return false if not supporting mobile app.
     * @return array
     */
    public function mobile_question_choices_display() {
        $choices = [];
        $cnum = 0;
        if ($this->has_choices()) {
            foreach ($this->choices as $choice) {
                $choices[$cnum] = clone($choice);
                $contents = questionnaire_choice_values($choice->content);
                $choices[$cnum]->content = format_text($contents->text, FORMAT_HTML, ['noclean' => true]).$contents->image;
                $cnum++;
            }
        }
        return $choices;
    }

    /**
     * Return a field key to be used by the mobile app.
     * @param int $choiceid
     * @return string
     */
    public function mobile_fieldkey($choiceid = 0) {
        $choicefield = '';
        if ($choiceid !== 0) {
            $choicefield = '_' . $choiceid;
        }
        return 'response_' . $this->type_id . '_' . $this->id . $choicefield;
    }

    /**
     * Return the mobile response data.
     * @param response $response
     * @return array
     */
    public function get_mobile_response_data($response) {
        $resultdata = [];
        if (isset($response->answers[$this->id][0])) {
            $resultdata[$this->mobile_fieldkey()] = $response->answers[$this->id][0]->value;
        } else {
            $resultdata[$this->mobile_fieldkey()] = false;
        }

        return $resultdata;
    }

    /**
     * True if question need extradata for mobile app.
     *
     * @return bool
     */
    public function mobile_question_extradata_display() {
        return false;
    }

    /**
     * Return the otherdata to be used by the mobile app.
     *
     * @return array
     */
    public function mobile_otherdata() {
        return [];
    }
}