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]).' '];$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' => ' '.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]).' ';$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 = ' '.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();}}