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;

/**
 * This file contains the parent class for rate 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
 */
class rate extends question {

    /** @var array $nameddegrees */
    public $nameddegrees = [];

    /**
     * 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 = array()) {
        $this->length = 5;
        parent::__construct($id, $question, $context, $params);
        $this->add_nameddegrees_from_extradata();
    }

    /**
     * Each question type must define its response class.
     * @return object The response object based off of questionnaire_response_base.
     */
    protected function responseclass() {
        return '\\mod_questionnaire\\responsetype\\rank';
    }

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

    /**
     * Return true if the question has choices.
     */
    public function has_choices() {
        return true;
    }

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

    /**
     * Override and return a response template if provided. Output of response_survey_display is iterpreted based on this.
     * @return boolean | string
     */
    public function response_template() {
        return 'mod_questionnaire/response_rate';
    }

    /**
     * Return true if rate scale type is set to "Normal".
     * @param int $scaletype
     * @return bool
     */
    public static function type_is_normal_rate_scale($scaletype) {
        return ($scaletype == 0);
    }

    /**
     * Return true if rate scale type is set to "N/A column".
     * @param int $scaletype
     * @return bool
     */
    public static function type_is_na_column($scaletype) {
        return ($scaletype == 1);
    }

    /**
     * Return true if rate scale type is set to "No duplicate choices".
     * @param int $scaletype
     * @return bool
     */
    public static function type_is_no_duplicate_choices($scaletype) {
        return ($scaletype == 2);
    }

    /**
     * Return true if rate scale type is set to "Osgood".
     * @param int $scaletype
     * @return bool
     */
    public static function type_is_osgood_rate_scale($scaletype) {
        return ($scaletype == 3);
    }

    /**
     * Return true if rate scale type is set to "Normal".
     * @return bool
     */
    public function normal_rate_scale() {
        return self::type_is_normal_rate_scale($this->precise);
    }

    /**
     * Return true if rate scale type is set to "N/A column".
     * @return bool
     */
    public function has_na_column() {
        return self::type_is_na_column($this->precise);
    }

    /**
     * Return true if rate scale type is set to "No duplicate choices".
     * @return bool
     */
    public function no_duplicate_choices() {
        return self::type_is_no_duplicate_choices($this->precise);
    }

    /**
     * Return true if rate scale type is set to "Osgood".
     * @return bool
     */
    public function osgood_rate_scale() {
        return self::type_is_osgood_rate_scale($this->precise);
    }

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

    /**
     * True if the question supports feedback and has valid settings for feedback. Override if the default logic is not enough.
     */
    public function valid_feedback() {
        return $this->supports_feedback() && $this->has_choices() && $this->required() && !empty($this->name) &&
            ($this->normal_rate_scale() || $this->osgood_rate_scale()) && !empty($this->nameddegrees);
    }

    /**
     * Get the maximum score possible for feedback if appropriate. Override if default behaviour is not correct.
     * @return int | boolean
     */
    public function get_feedback_maxscore() {
        if ($this->valid_feedback()) {
            $maxscore = 0;
            $nbchoices = count($this->choices);
            foreach ($this->nameddegrees as $value => $label) {
                if ($value > $maxscore) {
                    $maxscore = $value;
                }
            }
            // The maximum score needs to be multiplied by the number of items to rate.
            $maxscore = $maxscore * $nbchoices;
        } else {
            $maxscore = false;
        }
        return $maxscore;
    }

    /**
     * Return the context tags for the check question template.
     * @param \mod_questionnaire\responsetype\response\response $response
     * @param string $descendantsdata
     * @param boolean $blankquestionnaire
     * @return object The check question context tags.
     *
     * TODO: This function needs to be rewritten. It is a mess!
     *
     */
    protected function question_survey_display($response, $descendantsdata, $blankquestionnaire=false) {
        $choicetags = new \stdClass();
        $choicetags->qelements = [];

        $disabled = '';
        if ($blankquestionnaire) {
            $disabled = ' disabled="disabled"';
        }
        if (!empty($data) && ( !isset($data->{'q'.$this->id}) || !is_array($data->{'q'.$this->id}) ) ) {
            $data->{'q'.$this->id} = [];
        }

        // Check if rate question has one line only to display full width columns of choices.
        $nocontent = false;
        $nameddegrees = count($this->nameddegrees);
        $n = [];
        $v = [];
        $maxndlen = 0;
        foreach ($this->choices as $cid => $choice) {
            $content = $choice->content;
            if (!$nocontent && $content == '') {
                $nocontent = true;
            }
            if ($nameddegrees == 0) {
                // Determine if the choices have named values.
                $contents = questionnaire_choice_values($content);
                if ($contents->modname) {
                    $choice->content = $contents->text;
                }
            }
        }

        // The 0.1% right margin is needed to avoid the horizontal scrollbar in Chrome!
        // A one-line rate question (no content) does not need to span more than 50%.
        $width = $nocontent ? "50%" : "99.9%";
        $choicetags->qelements['twidth'] = $width;
        $choicetags->qelements['headerrow'] = [];
        // If Osgood, adjust central columns to width of named degrees if any.
        if ($this->osgood_rate_scale()) {
            if ($maxndlen < 4) {
                $width = 45;
            } else if ($maxndlen < 13) {
                $width = 40;
            } else {
                $width = 30;
            }
            $nn = 100 - ($width * 2);
            $colwidth = ($nn / $this->length).'%';
            $textalign = 'right';
            $width = $width . '%';
        } else if ($nocontent) {
            $width = '0%';
            $colwidth = (100 / $this->length).'%';
            $textalign = 'right';
        } else {
            $width = '59%';
            $colwidth = (40 / $this->length).'%';
            $textalign = 'left';
        }

        $choicetags->qelements['headerrow']['col1width'] = $width;

        if ($this->has_na_column()) {
            $na = get_string('notapplicable', 'questionnaire');
        } else {
            $na = '';
        }
        if ($this->no_duplicate_choices()) {
            $order = 'other_rate_uncheck(name, value)';
        } else {
            $order = '';
        }

        if (!$this->no_duplicate_choices()) {
            $nbchoices = count($this->choices);
        } else { // If "No duplicate choices", can restrict nbchoices to number of rate items specified.
            $nbchoices = $this->length;
        }

        // Display empty td for Not yet answered column.
        if (($nbchoices > 1) && !$this->no_duplicate_choices() && !$blankquestionnaire) {
            $choicetags->qelements['headerrow']['colnya'] = true;
        }

        $collabel = [];
        if ($nameddegrees > 0) {
            $currentdegree = reset($this->nameddegrees);
        }
        for ($j = 1; $j <= $this->length; $j++) {
            $col = [];
            if (($nameddegrees > 0) && ($currentdegree !== false)) {
                $str = format_text($currentdegree, FORMAT_HTML, ['noclean' => true]);
                $currentdegree = next($this->nameddegrees);
            } else {
                $str = $j;
            }
            $val = $j;
            if ($blankquestionnaire) {
                $val = '<br />('.$val.')';
            } else {
                $val = '';
            }
            $col['colwidth'] = $colwidth;
            $col['coltext'] = $str.$val;
            $collabel[$j] = $col['coltext'];
            $choicetags->qelements['headerrow']['cols'][] = $col;
        }
        if ($na) {
            $choicetags->qelements['headerrow']['cols'][] = ['colwidth' => $colwidth, 'coltext' => $na];
            $collabel[$j] = $na;
        }

        $num = 0;
        foreach ($this->choices as $cid => $choice) {
            $num += (isset($response->answers[$this->id][$cid]) && ($response->answers[$this->id][$cid]->value != -999));
        }

        $notcomplete = false;
        if ( ($num != $nbchoices) && ($num != 0) ) {
            $this->add_notification(get_string('checkallradiobuttons', 'questionnaire', $nbchoices));
            $notcomplete = true;
        }

        $row = 0;
        $choicetags->qelements['rows'] = [];
        foreach ($this->choices as $cid => $choice) {
            $cols = [];
            if (isset($choice->content)) {
                $row++;
                $str = 'q'."{$this->id}_$cid";
                $content = $choice->content;
                if ($this->osgood_rate_scale()) {
                    list($content, $contentright) = array_merge(preg_split('/[|]/', $content), array(' '));
                }
                $cols[] = ['colstyle' => 'text-align: '.$textalign.';',
                           'coltext' => format_text($content, FORMAT_HTML, ['noclean' => true]).'&nbsp;'];

                $bg = 'c0 raterow';
                if (($nbchoices > 1) && !$this->no_duplicate_choices()  && !$blankquestionnaire) {
                    $checked = ' checked="checked"';
                    $completeclass = 'notanswered';
                    $title = '';
                    if ($notcomplete && isset($response->answers[$this->id][$cid]) &&
                        ($response->answers[$this->id][$cid]->value == -999)) {
                        $completeclass = 'notcompleted';
                        $title = get_string('pleasecomplete', 'questionnaire');
                    }
                    // Set value of notanswered button to -999 in order to eliminate it from form submit later on.
                    $colinput = ['name' => $str, 'value' => -999];
                    if (!empty($checked)) {
                        $colinput['checked'] = true;
                    }
                    if (!empty($order)) {
                        $colinput['onclick'] = $order;
                    }
                    $cols[] = ['colstyle' => 'width:1%;', 'colclass' => $completeclass, 'coltitle' => $title,
                        'colinput' => $colinput];
                }
                if ($nameddegrees > 0) {
                    reset($this->nameddegrees);
                }
                for ($j = 1; $j <= $this->length + $this->has_na_column(); $j++) {
                    if (!isset($collabel[$j])) {
                        // If not using this value, continue.
                        continue;
                    }
                    $col = [];
                    $checked = '';
                    // If isna column then set na choice to -1 value. This needs work!
                    if (!empty($this->nameddegrees) && (key($this->nameddegrees) !== null)) {
                        $value = key($this->nameddegrees);
                        next($this->nameddegrees);
                    } else {
                        $value = ($j <= $this->length ? $j : -1);
                    }
                    if (isset($response->answers[$this->id][$cid]) && ($value == $response->answers[$this->id][$cid]->value)) {
                        $checked = ' checked="checked"';
                    }
                    $col['colstyle'] = 'text-align:center';
                    $col['colclass'] = $bg;
                    $col['colhiddentext'] = get_string('option', 'questionnaire', $j);
                    $col['colinput']['name'] = $str;
                    $col['colinput']['value'] = $value;
                    $col['colinput']['id'] = $str.'_'.$value;
                    if (!empty($checked)) {
                        $col['colinput']['checked'] = true;
                    }
                    if (!empty($disabled)) {
                        $col['colinput']['disabled'] = true;
                    }
                    if (!empty($order)) {
                        $col['colinput']['onclick'] = $order;
                    }
                    $col['colinput']['label'] = 'Choice '.$collabel[$j].' for row '.format_text($content, FORMAT_PLAIN);
                    if ($bg == 'c0 raterow') {
                        $bg = 'c1 raterow';
                    } else {
                        $bg = 'c0 raterow';
                    }
                    $cols[] = $col;
                }
                if ($this->osgood_rate_scale()) {
                    $cols[] = ['coltext' => '&nbsp;'.format_text($contentright, FORMAT_HTML, ['noclean' => true])];
                }
                $choicetags->qelements['rows'][] = ['cols' => $cols];
            }
        }

        return $choicetags;
    }

    /**
     * Return the context tags for the rate response template.
     * @param \mod_questionnaire\responsetype\response\response $response
     * @return \stdClass The rate question response context tags.
     * @throws \coding_exception
     */
    protected function response_survey_display($response) {
        static $uniquetag = 0;  // To make sure all radios have unique names.

        $resptags = new \stdClass();
        $resptags->headers = [];
        $resptags->rows = [];

        if (!isset($response->answers[$this->id])) {
            $response->answers[$this->id][] = new \mod_questionnaire\responsetype\answer\answer();
        }
        // Check if rate question has one line only to display full width columns of choices.
        $nocontent = false;
        foreach ($this->choices as $cid => $choice) {
            if ($choice->content == '') {
                $nocontent = true;
                break;
            }
        }
        $resptags->twidth = $nocontent ? "50%" : "99.9%";

        $bg = 'c0';
        $nameddegrees = 0;
        $cidnamed = array();
        // Max length of potential named degree in column head.
        $maxndlen = 0;
        if ($this->osgood_rate_scale()) {
            $resptags->osgood = 1;
            if ($maxndlen < 4) {
                $sidecolwidth = '45%';
                $sidecolwidthn = 45;
            } else if ($maxndlen < 13) {
                $sidecolwidth = '40%';
                $sidecolwidthn = 40;
            } else {
                $sidecolwidth = '30%';
                $sidecolwidthn = 30;
            }
            $nn = 100 - ($sidecolwidthn * 2);
            $resptags->sidecolwidth = $sidecolwidth;
            $resptags->colwidth = ($nn / $this->length).'%';
            $resptags->textalign = 'right';
        } else {
            $resptags->sidecolwidth = '49%';
            $resptags->colwidth = (50 / $this->length).'%';
            $resptags->textalign = 'left';
        }
        if (!empty($this->nameddegrees)) {
            $this->length = count($this->nameddegrees);
            reset($this->nameddegrees);
        }
        for ($j = 1; $j <= $this->length; $j++) {
            $cellobj = new \stdClass();
            $cellobj->bg = $bg;
            if (!empty($this->nameddegrees)) {
                $cellobj->str = current($this->nameddegrees);
                next($this->nameddegrees);
            } else {
                $cellobj->str = $j;
            }
            if ($bg == 'c0') {
                $bg = 'c1';
            } else {
                $bg = 'c0';
            }
            $resptags->headers[] = $cellobj;
        }
        if ($this->has_na_column()) {
            $cellobj = new \stdClass();
            $cellobj->bg = $bg;
            $cellobj->str = get_string('notapplicable', 'questionnaire');
            $resptags->headers[] = $cellobj;
        }

        foreach ($this->choices as $cid => $choice) {
            $rowobj = new \stdClass();
            // Do not print column names if named column exist.
            if (!array_key_exists($cid, $cidnamed)) {
                $str = 'q'."{$this->id}_$cid";
                $content = $choice->content;
                $contents = questionnaire_choice_values($content);
                if ($contents->modname) {
                    $content = $contents->text;
                }
                if ($this->osgood_rate_scale()) {
                    list($content, $contentright) = array_merge(preg_split('/[|]/', $content), array(' '));
                }
                $rowobj->content = format_text($content, FORMAT_HTML, ['noclean' => true]).'&nbsp;';
                $bg = 'c0';
                $cols = [];
                if (!empty($this->nameddegrees)) {
                    $this->length = count($this->nameddegrees);
                    reset($this->nameddegrees);
                }
                for ($j = 1; $j <= $this->length; $j++) {
                    $cellobj = new \stdClass();
                    if (isset($response->answers[$this->id][$cid])) {
                        if (!empty($this->nameddegrees)) {
                            if ($response->answers[$this->id][$cid]->value == key($this->nameddegrees)) {
                                $cellobj->checked = 1;
                            }
                            next($this->nameddegrees);
                        } else if ($j == $response->answers[$this->id][$cid]->value) {
                            $cellobj->checked = 1;
                        }
                    }
                    $cellobj->str = $str.$j.$uniquetag++;
                    $cellobj->bg = $bg;
                    // N/A column checked.
                    $checkedna = (isset($response->answers[$this->id][$cid]) && ($response->answers[$this->id][$cid]->value == -1));
                    if ($bg == 'c0') {
                        $bg = 'c1';
                    } else {
                        $bg = 'c0';
                    }
                    $cols[] = $cellobj;
                }
                if ($this->has_na_column()) { // N/A column.
                    $cellobj = new \stdClass();
                    if ($checkedna) {
                        $cellobj->checked = 1;
                    }
                    $cellobj->str = $str.$j.$uniquetag++.'na';
                    $cellobj->bg = $bg;
                    $cols[] = $cellobj;
                }
                $rowobj->cols = $cols;
                if ($this->osgood_rate_scale()) {
                    $rowobj->osgoodstr = '&nbsp;'.format_text($contentright, FORMAT_HTML, ['noclean' => true]);
                }
                $resptags->rows[] = $rowobj;
            }
        }
        return $resptags;
    }

    /**
     * Check question's form data for complete response.
     *
     * @param \stdClass $responsedata The data entered into the response.
     * @return boolean
     *
     */
    public function response_complete($responsedata) {
        if (!is_a($responsedata, 'mod_questionnaire\responsetype\response\response')) {
            $response = \mod_questionnaire\responsetype\response\response::response_from_webform($responsedata, [$this]);
        } else {
            $response = $responsedata;
        }

        // To make it easier, create an array of answers by choiceid.
        $answers = [];
        if (isset($response->answers[$this->id])) {
            foreach ($response->answers[$this->id] as $answer) {
                $answers[$answer->choiceid] = $answer;
            }
        }

        $answered = true;
        $num = 0;
        $nbchoices = count($this->choices);
        $na = get_string('notapplicable', 'questionnaire');
        foreach ($this->choices as $cid => $choice) {
            // In case we have named degrees on the Likert scale, count them to substract from nbchoices.
            $nameddegrees = 0;
            $content = $choice->content;
            if (preg_match("/^[0-9]{1,3}=/", $content)) {
                $nameddegrees++;
            } else {
                if (isset($answers[$cid]) && !empty($answers[$cid]) && ($answers[$cid]->value == $na)) {
                    $answers[$cid]->value = -1;
                }
                // If choice value == -999 this is a not yet answered choice.
                $num += (isset($answers[$cid]) && ($answers[$cid]->value != -999));
            }
            $nbchoices -= $nameddegrees;
        }

        if ($num == 0) {
            if ($this->required()) {
                $answered = false;
            }
        }
        return $answered;
    }

    /**
     * Check question's form data for valid response. Override this is type has specific format requirements.
     *
     * @param \stdClass $responsedata The data entered into the response.
     * @return boolean
     */
    public function response_valid($responsedata) {
        // Work with a response object.
        if (!is_a($responsedata, 'mod_questionnaire\responsetype\response\response')) {
            $response = \mod_questionnaire\responsetype\response\response::response_from_webform($responsedata, [$this]);
        } else {
            $response = $responsedata;
        }
        $num = 0;
        $nbchoices = count($this->choices);
        $na = get_string('notapplicable', 'questionnaire');

        // Create an answers array indexed by choiceid for ease.
        $answers = [];
        $nodups = [];
        if (isset($response->answers[$this->id])) {
            foreach ($response->answers[$this->id] as $answer) {
                $answers[$answer->choiceid] = $answer;
                $nodups[] = $answer->value;
            }
        }

        foreach ($this->choices as $cid => $choice) {
            // In case we have named degrees on the Likert scale, count them to substract from nbchoices.
            $nameddegrees = 0;
            $content = $choice->content;
            if (preg_match("/^[0-9]{1,3}=/", $content)) {
                $nameddegrees++;
            } else {
                if (isset($answers[$cid]) && ($answers[$cid]->value == $na)) {
                    $answers[$cid]->value = -1;
                }
                // If choice value == -999 this is a not yet answered choice.
                $num += (isset($answers[$cid]) && ($answers[$cid]->value != -999));
            }
            $nbchoices -= $nameddegrees;
        }
        // If nodupes and nb choice restricted, nbchoices may be > actual choices, so limit it to $question->length.
        $isrestricted = ($this->length < count($this->choices)) && $this->no_duplicate_choices();
        if ($isrestricted) {
            $nbchoices = min ($nbchoices, $this->length);
        }

        // Test for duplicate answers in a no duplicate question type.
        if ($this->no_duplicate_choices()) {
            foreach ($answers as $answer) {
                if (count(array_keys($nodups, $answer->value)) > 1) {
                    return false;
                }
            }
        }

        if (($num != $nbchoices) && ($num != 0)) {
            return false;
        } else {
            return parent::response_valid($responsedata);
        }
    }

    /**
     * Return the length form element.
     * @param \MoodleQuickForm $mform
     * @param string $helptext
     */
    protected function form_length(\MoodleQuickForm $mform, $helptext = '') {
        return parent::form_length($mform, 'numberscaleitems');
    }

    /**
     * Return the precision form element.
     * @param \MoodleQuickForm $mform
     * @param string $helptext
     */
    protected function form_precise(\MoodleQuickForm $mform, $helptext = '') {
        $precoptions = array("0" => get_string('normal', 'questionnaire'),
                             "1" => get_string('notapplicablecolumn', 'questionnaire'),
                             "2" => get_string('noduplicates', 'questionnaire'),
                             "3" => get_string('osgood', 'questionnaire'));
        $mform->addElement('select', 'precise', get_string('kindofratescale', 'questionnaire'), $precoptions);
        $mform->addHelpButton('precise', 'kindofratescale', 'questionnaire');
        $mform->setType('precise', PARAM_INT);

        return $mform;
    }

    /**
     * Override if the question uses the extradata field.
     * @param \MoodleQuickForm $mform
     * @param string $helpname
     * @return \MoodleQuickForm
     */
    protected function form_extradata(\MoodleQuickForm $mform, $helpname = '') {
        $defaultvalue = '';
        foreach ($this->nameddegrees as $value => $label) {
            $defaultvalue .= $value . '=' . $label . "\n";
        }

        $options = ['wrap' => 'virtual'];
        $mform->addElement('textarea', 'allnameddegrees', get_string('allnameddegrees', 'questionnaire'), $options);
        $mform->setDefault('allnameddegrees', $defaultvalue);
        $mform->setType('allnameddegrees', PARAM_RAW);
        $mform->addHelpButton('allnameddegrees', 'allnameddegrees', 'questionnaire');

        return $mform;
    }

    /**
     * Any preprocessing of general data.
     * @param \stdClass $formdata
     * @return bool
     */
    protected function form_preprocess_data($formdata) {
        $nameddegrees = [];
        // Named degrees are put one per line in the form "[value]=[label]".
        if (!empty($formdata->allnameddegrees)) {
            $nameddegreelines = explode("\n", $formdata->allnameddegrees);
            foreach ($nameddegreelines as $nameddegreeline) {
                $nameddegreeline = trim($nameddegreeline);
                if (($nameddegree = \mod_questionnaire\question\choice::content_is_named_degree_choice($nameddegreeline)) !==
                    false) {
                    $nameddegrees += $nameddegree;
                }
            }
        }

        // Now store the new named degrees in extradata.
        $formdata->extradata = json_encode($nameddegrees);
        return parent::form_preprocess_data($formdata);
    }

    /**
     * Override this function for question specific choice preprocessing.
     * @param \stdClass $formdata
     * @return false
     */
    protected function form_preprocess_choicedata($formdata) {
        if (empty($formdata->allchoices)) {
            // Add dummy blank space character for empty value.
            $formdata->allchoices = " ";
        } else {
            $allchoices = $formdata->allchoices;
            $allchoices = explode("\n", $allchoices);
            $ispossibleanswer = false;
            $nbnameddegrees = 0;
            $nbvalues = 0;
            foreach ($allchoices as $choice) {
                if ($choice) {
                    // Check for number from 1 to 3 digits, followed by the equal sign =.
                    if (preg_match("/^[0-9]{1,3}=/", $choice)) {
                        $nbnameddegrees++;
                    } else {
                        $nbvalues++;
                        $ispossibleanswer = true;
                    }
                }
            }
            // Add carriage return and dummy blank space character for empty value.
            if (!$ispossibleanswer) {
                $formdata->allchoices .= "\n ";
            }

            // Sanity checks for correct number of values in $formdata->length.

            // Sanity check for named degrees.
            if ($nbnameddegrees && $nbnameddegrees != $formdata->length) {
                $formdata->length = $nbnameddegrees;
            }
            // Sanity check for "no duplicate choices"".
            if (self::type_is_no_duplicate_choices($formdata->precise) && ($formdata->length > $nbvalues || !$formdata->length)) {
                $formdata->length = $nbvalues;
            }
        }
        return true;
    }

    /**
     * Update the choice with the choicerecord.
     * @param \stdClass $choicerecord
     * @return bool
     */
    public function update_choice($choicerecord) {
        if ($nameddegree = \mod_questionnaire\question\choice::content_is_named_degree_choice($choicerecord->content)) {
            // Preserve any existing value from the new array.
            $this->nameddegrees = $nameddegree + $this->nameddegrees;
            $this->insert_nameddegrees($this->nameddegrees);
        }
        return parent::update_choice($choicerecord);
    }

    /**
     * Add a new choice to the database.
     * @param \stdClass $choicerecord
     * @return bool
     */
    public function add_choice($choicerecord) {
        if ($nameddegree = \mod_questionnaire\question\choice::content_is_named_degree_choice($choicerecord->content)) {
            // Preserve any existing value from the new array.
            $this->nameddegrees = $nameddegree + $this->nameddegrees;
            $this->insert_nameddegrees($this->nameddegrees);
        }
        return parent::add_choice($choicerecord);
    }

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

    /**
     * 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) {
        $mobiledata = parent::mobile_question_display($qnum, $autonum);
        $mobiledata->rates = $this->mobile_question_rates_display();
        if ($this->has_na_column()) {
            $mobiledata->hasnacolumn = (object)['value' => -1, 'label' => get_string('notapplicable', 'questionnaire')];
        }

        $mobiledata->israte = true;
        return $mobiledata;
    }

    /**
     * Override and return false if not supporting mobile app.
     * @return array
     */
    public function mobile_question_choices_display() {
        $choices = [];
        $excludes = [];
        $vals = $extracontents = [];
        $cnum = 0;
        foreach ($this->choices as $choiceid => $choice) {
            $choice->na = false;
            $choice->choice_id = $choiceid;
            $choice->id = $choiceid;
            $choice->question_id = $this->id;

            // Add a fieldkey for each choice.
            $choice->fieldkey = $this->mobile_fieldkey($choiceid);

            if ($this->osgood_rate_scale()) {
                list($choice->leftlabel, $choice->rightlabel) = array_merge(preg_split('/[|]/', $choice->content), []);
            }

            if ($this->normal_rate_scale() || $this->no_duplicate_choices()) {
                $choices[$cnum] = $choice;
                if ($this->required()) {
                    $choices[$cnum]->min = 0;
                    $choices[$cnum]->minstr = 1;
                } else {
                    $choices[$cnum]->min = 0;
                    $choices[$cnum]->minstr = 1;
                }
                $choices[$cnum]->max = intval($this->length) - 1;
                $choices[$cnum]->maxstr = intval($this->length);

            } else if ($this->has_na_column()) {
                $choices[$cnum] = $choice;
                if ($this->required()) {
                    $choices[$cnum]->min = 0;
                    $choices[$cnum]->minstr = 1;
                } else {
                    $choices[$cnum]->min = 0;
                    $choices[$cnum]->minstr = 1;
                }
                $choices[$cnum]->max = intval($this->length);
                $choices[$cnum]->na = true;

            } else {
                $excludes[$choiceid] = $choiceid;
                if ($choice->value == null) {
                    if ($arr = explode('|', $choice->content)) {
                        if (count($arr) == 2) {
                            $choices[$cnum] = $choice;
                            $choices[$cnum]->content = '';
                            $choices[$cnum]->minstr = $arr[0];
                            $choices[$cnum]->maxstr = $arr[1];
                        }
                    }
                } else {
                    $val = intval($choice->value);
                    $vals[$val] = $val;
                    $extracontents[] = $choice->content;
                }
            }
            if ($vals) {
                if ($q = $choices) {
                    foreach (array_keys($q) as $itemid) {
                        $choices[$itemid]->min = min($vals);
                        $choices[$itemid]->max = max($vals);
                    }
                }
            }
            if ($extracontents) {
                $extracontents = array_unique($extracontents);
                $extrahtml = '<br><ul>';
                foreach ($extracontents as $extracontent) {
                    $extrahtml .= '<li>'.$extracontent.'</li>';
                }
                $extrahtml .= '</ul>';
                $options = ['noclean' => true, 'para' => false, 'filter' => true,
                    'context' => $this->context, 'overflowdiv' => true];
                $choice->content .= format_text($extrahtml, FORMAT_HTML, $options);
            }

            if (!in_array($choiceid, $excludes)) {
                $choice->choice_id = $choiceid;
                if ($choice->value == null) {
                    $choice->value = '';
                }
                $choices[$cnum] = $choice;
            }
            $cnum++;
        }

        return $choices;
    }

    /**
     * Display the rates question for mobile.
     * @return array
     */
    public function mobile_question_rates_display() {
        $rates = [];
        if (!empty($this->nameddegrees)) {
            foreach ($this->nameddegrees as $value => $label) {
                $rates[] = (object)['value' => $value, 'label' => $label];
            }
        } else {
            for ($i = 1; $i <= $this->length; $i++) {
                $rates[] = (object)['value' => $i, 'label' => $i];
            }
        }
        return $rates;
    }

    /**
     * Return the mobile response data.
     * @param response $response
     * @return array
     */
    public function get_mobile_response_data($response) {
        $resultdata = [];
        if (isset($response->answers[$this->id])) {
            foreach ($response->answers[$this->id] as $answer) {
                // Add a fieldkey for each choice.
                if (!empty($this->nameddegrees)) {
                    if (isset($this->nameddegrees[$answer->value])) {
                        $resultdata[$this->mobile_fieldkey($answer->choiceid)] = $this->nameddegrees[$answer->value];
                    } else {
                        $resultdata[$this->mobile_fieldkey($answer->choiceid)] = $answer->value;
                    }
                } else {
                    $resultdata[$this->mobile_fieldkey($answer->choiceid)] = $answer->value;
                }
            }
        }
        return $resultdata;
    }

    /**
     * Add the nameddegrees property.
     */
    private function add_nameddegrees_from_extradata() {
        if (!empty($this->extradata)) {
            $this->nameddegrees = json_decode($this->extradata, true);
        }
    }

    /**
     * Insert nameddegress to the extradata database field.
     * @param array $nameddegrees
     * @return bool
     * @throws \dml_exception
     */
    public function insert_nameddegrees(array $nameddegrees) {
        return $this->insert_extradata(json_encode($nameddegrees));
    }

    /**
     * Helper function used to move existing named degree choices for the specified question from the "quest_choice" table to the
     * "question" table.
     * @param int $qid
     * @param null|\stdClass $questionrec
     */
    public static function move_nameddegree_choices(int $qid = 0, \stdClass $questionrec = null) {
        global $DB;

        if ($qid !== 0) {
            $question = new rate($qid);
        } else {
            $question = new rate(0, $questionrec);
        }
        $nameddegrees = [];
        $oldchoiceids = [];
        // There was an issue where rate values were being stored as 1..n, no matter what the named degree value was. We need to fix
        // the old responses now. This also assumes that the values are now 1 based rather than 0 based.
        $newvalues = [];
        $oldval = 1;
        foreach ($question->choices as $choice) {
            if ($nameddegree = $choice->is_named_degree_choice()) {
                $nameddegrees += $nameddegree;
                $oldchoiceids[] = $choice->id;
                reset($nameddegree);
                $newvalues[$oldval++] = key($nameddegree);
            }
        }

        if (!empty($nameddegrees)) {
            if ($question->insert_nameddegrees($nameddegrees)) {
                // Remove the old named desgree from the choices table.
                foreach ($oldchoiceids as $choiceid) {
                    \mod_questionnaire\question\choice::delete_from_db_by_id($choiceid);
                }

                // First get all existing rank responses for this question.
                $responses = $DB->get_recordset('questionnaire_response_rank', ['question_id' => $question->id]);
                // Iterating over each response record ensures we won't change an existing record more than once.
                foreach ($responses as $response) {
                    // Then, if the old value exists, set it to the new one.
                    if (isset($newvalues[$response->rankvalue])) {
                        $DB->set_field('questionnaire_response_rank', 'rankvalue', $newvalues[$response->rankvalue],
                            ['id' => $response->id]);
                    }
                }
                $responses->close();
            }
        }
    }

    /**
     * Helper function to move named degree choices for all questions, optionally for a specific surveyid.
     * This should only be called for an upgrade from before '2018110103', or from a restore operation for a version of a
     * questionnaire before '2018110103'.
     * @param int|null $surveyid
     */
    public static function move_all_nameddegree_choices(int $surveyid = null) {
        global $DB;

        // This operation might take a while. Cancel PHP timeouts for this.
        \core_php_time_limit::raise();

        // First, let's adjust all rate answers from zero based to one based (see GHI223).
        // If a specific survey is being dealt with, only use the questions from that survey.
        $skip = false;
        if ($surveyid !== null) {
            $qids = $DB->get_records_menu('questionnaire_question', ['surveyid' => $surveyid, 'type_id' => QUESRATE],
                '', 'id,surveyid');
            if (!empty($qids)) {
                list($qsql, $qparams) = $DB->get_in_or_equal(array_keys($qids));
            } else {
                // No relevant questions, so no need to do this step.
                $skip = true;
            }
        }

        // If we're doing this step, let's do it.
        if (!$skip) {
            $select = 'UPDATE {questionnaire_response_rank} ' .
                'SET rankvalue = (rankvalue + 1) ' .
                'WHERE (rankvalue >= 0)';
            if ($surveyid !== null) {
                $select .= ' AND (question_id ' . $qsql . ')';
            } else {
                $qparams = [];
            }
            $DB->execute($select, $qparams);
        }

        $args = ['type_id' => QUESRATE];
        if ($surveyid !== null) {
            $args['surveyid'] = $surveyid;
        }
        $ratequests = $DB->get_recordset('questionnaire_question', $args);
        foreach ($ratequests as $questionrec) {
            self::move_nameddegree_choices(0, $questionrec);
        }
        $ratequests->close();
    }
}