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\responsetype;

use Composer\Package\Package;
use mod_questionnaire\db\bulk_sql_config;

/**
 * Class for rank responses.
 *
 * @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 rank extends responsetype {
    /**
     * Provide the necessary response data table name. Should probably always be used with late static binding 'static::' form
     * rather than 'self::' form to allow for class extending.
     *
     * @return string response table name.
     */
    public static function response_table() {
        return 'questionnaire_response_rank';
    }

    /**
     * Provide an array of answer objects from web form data for the question.
     *
     * @param \stdClass $responsedata All of the responsedata as an object.
     * @param \mod_questionnaire\question\question $question
     * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects.
     * @throws \coding_exception
     */
    public static function answers_from_webform($responsedata, $question) {
        $answers = [];
        foreach ($question->choices as $cid => $choice) {
            $other = isset($responsedata->{'q' . $question->id . '_' . $cid}) ?
                $responsedata->{'q' . $question->id . '_' . $cid} : null;
            // Choice not set or not answered.
            if (!isset($other) || $other == '') {
                continue;
            }
            if ($other == get_string('notapplicable', 'questionnaire')) {
                $rank = -1;
            } else {
                $rank = intval($other);
            }
            $record = new \stdClass();
            $record->responseid = $responsedata->rid;
            $record->questionid = $question->id;
            $record->choiceid = $cid;
            $record->value = $rank;
            $answers[$cid] = answer\answer::create_from_data($record);
        }
        return $answers;
    }

    /**
     * Provide an array of answer objects from mobile data for the question.
     *
     * @param \stdClass $responsedata All of the responsedata as an object.
     * @param \mod_questionnaire\question\question $question
     * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects.
     */
    public static function answers_from_appdata($responsedata, $question) {
        $answers = [];
        if (isset($responsedata->{'q'.$question->id}) && !empty($responsedata->{'q'.$question->id})) {
            foreach ($responsedata->{'q' . $question->id} as $choiceid => $choicevalue) {
                if (isset($question->choices[$choiceid])) {
                    $record = new \stdClass();
                    $record->responseid = $responsedata->rid;
                    $record->questionid = $question->id;
                    $record->choiceid = $choiceid;
                    if (!empty($question->nameddegrees)) {
                        // If using named degrees, the app returns the label string. Find the value.
                        $nameddegreevalue = array_search($choicevalue, $question->nameddegrees);
                        if ($nameddegreevalue !== false) {
                            $choicevalue = $nameddegreevalue;
                        }
                    }
                    $record->value = $choicevalue;
                    $answers[] = answer\answer::create_from_data($record);
                }
            }
        }
        return $answers;
    }

    /**
     * Insert a provided response to the question.
     *
     * @param object $responsedata All of the responsedata as an object.
     * @return int|bool - on error the subtype should call set_error and return false.
     */
    public function insert_response($responsedata) {
        global $DB;

        if (!$responsedata instanceof \mod_questionnaire\responsetype\response\response) {
            $response = \mod_questionnaire\responsetype\response\response::response_from_webform($responsedata, [$this->question]);
        } else {
            $response = $responsedata;
        }

        $resid = false;

        if (isset($response->answers[$this->question->id])) {
            foreach ($response->answers[$this->question->id] as $answer) {
                // Record the choice selection.
                $record = new \stdClass();
                $record->response_id = $response->id;
                $record->question_id = $this->question->id;
                $record->choice_id = $answer->choiceid;
                $record->rankvalue = $answer->value;
                $resid = $DB->insert_record(static::response_table(), $record);
            }
        }
        return $resid;
    }

    /**
     * @param bool $rids
     * @param bool $anonymous
     * @return array
     *
     * TODO - This works differently than all other get_results methods. This needs to be refactored.
     */
    public function get_results($rids=false, $anonymous=false) {
        global $DB;

        $rsql = '';
        if (!empty($rids)) {
            list($rsql, $params) = $DB->get_in_or_equal($rids);
            $rsql = ' AND response_id ' . $rsql;
        }

        $select = 'question_id=' . $this->question->id . ' AND content NOT LIKE \'!other%\' ORDER BY id ASC';
        if ($rows = $DB->get_records_select('questionnaire_quest_choice', $select)) {
            foreach ($rows as $row) {
                $this->counts[$row->content] = new \stdClass();
                $nbna = $DB->count_records(static::response_table(), array('question_id' => $this->question->id,
                                'choice_id' => $row->id, 'rankvalue' => '-1'));
                $this->counts[$row->content]->nbna = $nbna;
            }
        }

        // For nameddegrees, need an array by degree value of positions (zero indexed).
        $rankvalue = [];
        if (!empty($this->question->nameddegrees)) {
            $rankvalue = array_flip(array_keys($this->question->nameddegrees));
        }

        $isrestricted = ($this->question->length < count($this->question->choices)) && $this->question->no_duplicate_choices();
        // Usual case.
        if (!$isrestricted) {
            if (!empty ($rankvalue)) {
                $sql = "SELECT r.id, c.content, r.rankvalue, c.id AS choiceid
                FROM {questionnaire_quest_choice} c, {".static::response_table()."} r
                WHERE r.choice_id = c.id
                AND c.question_id = " . $this->question->id . "
                AND r.rankvalue >= 0{$rsql}
                ORDER BY choiceid";
                $results = $DB->get_records_sql($sql, $params);
                $value = [];
                foreach ($results as $result) {
                    if (isset($rankvalue[$result->rankvalue])) {
                        if (isset ($value[$result->choiceid])) {
                            $value[$result->choiceid] += $rankvalue[$result->rankvalue] + 1;
                        } else {
                            $value[$result->choiceid] = $rankvalue[$result->rankvalue] + 1;
                        }
                    }
                }
            }

            $sql = "SELECT c.id, c.content, a.average, a.num
                    FROM {questionnaire_quest_choice} c
                    INNER JOIN
                         (SELECT c2.id, AVG(a2.rankvalue) AS average, COUNT(a2.response_id) AS num
                          FROM {questionnaire_quest_choice} c2, {".static::response_table()."} a2
                          WHERE c2.question_id = ? AND a2.question_id = ? AND a2.choice_id = c2.id AND a2.rankvalue >= 0{$rsql}
                          GROUP BY c2.id) a ON a.id = c.id
                          order by c.id";
            $results = $DB->get_records_sql($sql, array_merge(array($this->question->id, $this->question->id), $params));
            if (!empty ($rankvalue)) {
                foreach ($results as $key => $result) {
                    if (isset($value[$key])) {
                        $result->averagevalue = $value[$key] / $result->num;
                    }
                }
            }
            // Reindex by 'content'. Can't do this from the query as it won't work with MS-SQL.
            foreach ($results as $key => $result) {
                $results[$result->content] = $result;
                unset($results[$key]);
            }
            return $results;
            // Case where scaleitems is less than possible choices.
        } else {
            $sql = "SELECT c.id, c.content, a.sum, a.num
                    FROM {questionnaire_quest_choice} c
                    INNER JOIN
                         (SELECT c2.id, SUM(a2.rankvalue) AS sum, COUNT(a2.response_id) AS num
                          FROM {questionnaire_quest_choice} c2, {".static::response_table()."} a2
                          WHERE c2.question_id = ? AND a2.question_id = ? AND a2.choice_id = c2.id AND a2.rankvalue >= 0{$rsql}
                          GROUP BY c2.id) a ON a.id = c.id";
            $results = $DB->get_records_sql($sql, array_merge(array($this->question->id, $this->question->id), $params));
            // Formula to calculate the best ranking order.
            $nbresponses = count($rids);
            foreach ($results as $key => $result) {
                $result->average = ($result->sum + ($nbresponses - $result->num) * ($this->length + 1)) / $nbresponses;
                $results[$result->content] = $result;
                unset($results[$key]);
            }
            return $results;
        }
    }

    /**
     * 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 | boolean
     */
    public function get_feedback_scores(array $rids) {
        global $DB;

        $rsql = '';
        $params = [$this->question->id];
        if (!empty($rids)) {
            list($rsql, $rparams) = $DB->get_in_or_equal($rids);
            $params = array_merge($params, $rparams);
            $rsql = ' AND response_id ' . $rsql;
        }
        $params[] = 'y';

        $sql = 'SELECT r.id, r.response_id as rid, r.question_id AS qid, r.choice_id AS cid, r.rankvalue ' .
            'FROM {'.$this->response_table().'} r ' .
            'INNER JOIN {questionnaire_quest_choice} c ON r.choice_id = c.id ' .
            'WHERE r.question_id= ? ' . $rsql . ' ' .
            'ORDER BY rid,cid ASC';
        $responses = $DB->get_recordset_sql($sql, $params);

        $rid = 0;
        $feedbackscores = [];
        foreach ($responses as $response) {
            if ($rid != $response->rid) {
                $rid = $response->rid;
                $feedbackscores[$rid] = new \stdClass();
                $feedbackscores[$rid]->rid = $rid;
                $feedbackscores[$rid]->score = 0;
            }
            // Only count scores that are currently defined (in case old responses are using older data).
            $feedbackscores[$rid]->score += isset($this->question->nameddegrees[$response->rankvalue]) ? $response->rankvalue : 0;
        }

        return (!empty($feedbackscores) ? $feedbackscores : false);
    }

    /**
     * Provide a template for results screen if defined.
     * @param bool $pdf
     * @return mixed The template string or false/
     */
    public function results_template($pdf = false) {
        if ($pdf) {
            return 'mod_questionnaire/resultspdf_rate';
        } else {
            return 'mod_questionnaire/results_rate';
        }
    }

    /**
     * Provide the result information for the specified result records.
     *
     * @param int|array $rids - A single response id, or array.
     * @param string $sort - Optional display sort.
     * @param boolean $anonymous - Whether or not responses are anonymous.
     * @return string - Display output.
     */
    public function display_results($rids=false, $sort='', $anonymous=false) {
        $output = '';

        if (is_array($rids)) {
            $prtotal = 1;
        } else if (is_int($rids)) {
            $prtotal = 0;
        }

        if ($rows = $this->get_results($rids, $sort, $anonymous)) {
            $stravgvalue = ''; // For printing table heading.
            foreach ($this->counts as $key => $value) {
                $ccontent = $key;
                $avgvalue = '';
                if (array_key_exists($ccontent, $rows)) {
                    $avg = $rows[$ccontent]->average;
                    $this->counts[$ccontent]->num = $rows[$ccontent]->num;
                    if (isset($rows[$ccontent]->averagevalue)) {
                        $avgvalue = $rows[$ccontent]->averagevalue;
                        $osgood = false;
                        if ($this->question->osgood_rate_scale()) { // Osgood's semantic differential.
                            $osgood = true;
                        }
                        if ($stravgvalue == '' && !$osgood) {
                            $stravgvalue = ' ('.get_string('andaveragevalues', 'questionnaire').')';
                        }
                    } else {
                        $avgvalue = null;
                    }
                } else {
                    $avg = 0;
                }
                $this->counts[$ccontent]->avg = $avg;
                $this->counts[$ccontent]->avgvalue = $avgvalue;
            }
            $output1 = $this->mkresavg($sort, $stravgvalue);
            $output2 = $this->mkrescount($rids, $rows, $sort);
            $output = (object)array_merge((array)$output1, (array)$output2);
        } else {
            $output = (object)['noresponses' => true];
        }
        return $output;
    }

    /**
     * Return an array of answers by question/choice for the given response. Must be implemented by the subclass.
     *
     * @param int $rid The response id.
     * @return array
     */
    public static function response_select($rid) {
        global $DB;

        $values = [];
        $sql = 'SELECT a.id as aid, q.id AS qid, q.precise AS precise, c.id AS cid, q.content, c.content as ccontent,
                                a.rankvalue as arank '.
            'FROM {'.static::response_table().'} a, {questionnaire_question} q, {questionnaire_quest_choice} c '.
            'WHERE a.response_id= ? AND a.question_id=q.id AND a.choice_id=c.id '.
            'ORDER BY aid, a.question_id, c.id';
        $records = $DB->get_records_sql($sql, [$rid]);
        foreach ($records as $row) {
            // Next two are 'qid' and 'cid', each with numeric and hash keys.
            $osgood = false;
            if (\mod_questionnaire\question\rate::type_is_osgood_rate_scale($row->precise)) {
                $osgood = true;
            }
            $qid = $row->qid.'_'.$row->cid;
            unset($row->aid); // Get rid of the answer id.
            unset($row->qid);
            unset($row->cid);
            unset($row->precise);
            $row = (array)$row;
            $newrow = [];
            foreach ($row as $key => $val) {
                if ($key != 'content') { // No need to keep question text - ony keep choice text and rank.
                    if ($key == 'ccontent') {
                        if ($osgood) {
                            list($contentleft, $contentright) = array_merge(preg_split('/[|]/', $val), [' ']);
                            $contents = questionnaire_choice_values($contentleft);
                            if ($contents->title) {
                                $contentleft = $contents->title;
                            }
                            $contents = questionnaire_choice_values($contentright);
                            if ($contents->title) {
                                $contentright = $contents->title;
                            }
                            $val = strip_tags($contentleft.'|'.$contentright);
                            $val = preg_replace("/[\r\n\t]/", ' ', $val);
                        } else {
                            $contents = questionnaire_choice_values($val);
                            if ($contents->modname) {
                                $val = $contents->modname;
                            } else if ($contents->title) {
                                $val = $contents->title;
                            } else if ($contents->text) {
                                $val = strip_tags($contents->text);
                                $val = preg_replace("/[\r\n\t]/", ' ', $val);
                            }
                        }
                    }
                    $newrow[] = $val;
                }
            }
            $values[$qid] = $newrow;
        }

        return $values;
    }

    /**
     * Return an array of answer objects by question for the given response id.
     * THIS SHOULD REPLACE response_select.
     *
     * @param int $rid The response id.
     * @return array array answer
     * @throws \dml_exception
     */
    public static function response_answers_by_question($rid) {
        global $DB;

        $answers = [];
        $sql = 'SELECT id, response_id as responseid, question_id as questionid, choice_id as choiceid, rankvalue as value ' .
            'FROM {' . static::response_table() .'} ' .
            'WHERE response_id = ? ';
        $records = $DB->get_records_sql($sql, [$rid]);
        foreach ($records as $record) {
            $answers[$record->questionid][$record->choiceid] = answer\answer::create_from_data($record);
        }

        return $answers;
    }

    /**
     * Configure bulk sql
     * @return bulk_sql_config
     */
    protected function bulk_sql_config() {
        return new bulk_sql_config(static::response_table(), 'qrr', true, false, true);
    }

    /**
     * Return a structure for averages.
     * @param string $sort
     * @param string $stravgvalue
     * @return \stdClass
     */
    private function mkresavg($sort, $stravgvalue='') {
        global $CFG;

        $stravgrank = get_string('averagerank', 'questionnaire');
        $osgood = false;
        if ($this->question->precise == 3) { // Osgood's semantic differential.
            $osgood = true;
            $stravgrank = get_string('averageposition', 'questionnaire');
        }
        $stravg = '<div style="text-align:right">'.$stravgrank.$stravgvalue.'</div>';

        $isna = $this->question->precise == 1;
        $isnahead = '';
        $nbchoices = count($this->counts);
        $isrestricted = ($this->question->length < $nbchoices) && $this->question->precise == 2;

        if ($isna) {
            $isnahead = get_string('notapplicable', 'questionnaire');
        }
        $pagetags = new \stdClass();
        $pagetags->averages = new \stdClass();

        if ($isna) {
            $header1 = new \stdClass();
            $header1->text = '';
            $header1->align = '';
            $header2 = new \stdClass();
            $header2->text = $stravg;
            $header2->align = '';
            $header3 = new \stdClass();
            $header3->text = '&dArr;';
            $header3->align = 'center';
            $header4 = new \stdClass();
            $header4->text = $isnahead;
            $header4->align = 'right';
        } else {
            if ($osgood) {
                $stravg = '<div style="text-align:center">'.$stravgrank.'</div>';
                $header1 = new \stdClass();
                $header1->text = '';
                $header1->align = '';
                $header2 = new \stdClass();
                $header2->text = $stravg;
                $header2->align = '';
                $header3 = new \stdClass();
                $header3->text = '';
                $header3->align = 'center';
            } else {
                $header1 = new \stdClass();
                $header1->text = '';
                $header1->align = '';
                $header2 = new \stdClass();
                $header2->text = $stravg;
                $header2->align = '';
                $header3 = new \stdClass();
                $header3->text = '&dArr;';
                $header3->align = 'center';
            }
        }
        // PDF columns are based on a 11.69in x 8.27in page. Margins are 15mm each side, or 1.1811 in total.
        $pdfwidth = 11.69 - 1.1811;
        if ($isna) {
            $header1->width = '55%';
            $header2->width = '35%';
            $header3->width = '5%';
            $header4->width = '5%';
            $header1->pdfwidth = $pdfwidth * .55;
            $header2->pdfwidth = $pdfwidth * .35;
            $header3->pdfwidth = $pdfwidth * .05;
            $header4->pdfwidth = $pdfwidth * .05;
        } else if ($osgood) {
            $header1->width = '25%';
            $header2->width = '50%';
            $header3->width = '25%';
            $header1->pdfwidth = $pdfwidth * .25;
            $header2->pdfwidth = $pdfwidth * .5;
            $header3->pdfwidth = $pdfwidth * .25;
        } else {
            $header1->width = '60%';
            $header2->width = '35%';
            $header3->width = '5%';
            $header1->pdfwidth = $pdfwidth * .6;
            $header2->pdfwidth = $pdfwidth * .35;
            $header3->pdfwidth = $pdfwidth * .05;
        }
        $pagetags->averages->headers = [$header1, $header2, $header3];
        if (isset($header4)) {
            $pagetags->averages->headers[] = $header4;
        }

        $imageurl = $CFG->wwwroot.'/mod/questionnaire/images/hbar.gif';
        $spacerimage = $CFG->wwwroot . '/mod/questionnaire/images/hbartransp.gif';
        $llength = $this->question->length;
        if (!$llength) {
            $llength = 5;
        }
        // Add an extra column to accomodate lower ranks in this case.
        $llength += $isrestricted;
        $width = 100 / $llength;
        $n = array();
        $nameddegrees = 0;
        foreach ($this->question->nameddegrees as $degree) {
            // To take into account languages filter.
            $content = (format_text($degree, FORMAT_HTML, ['noclean' => true]));
            $n[$nameddegrees] = $degree;
            $nameddegrees++;
        }
        for ($j = 0; $j < $this->question->length; $j++) {
            if (isset($n[$j])) {
                $str = $n[$j];
            } else {
                $str = $j + 1;
            }
        }
        $rankcols = [];
        $pdfwidth = $header2->pdfwidth / (100 / $width);
        for ($i = 0; $i <= $llength - 1; $i++) {
            if ($isrestricted && $i == $llength - 1) {
                $str = "...";
                $rankcols[] = (object)['width' => $width . '%', 'text' => '...', 'pdfwidth' => $pdfwidth];
            } else if (isset($n[$i])) {
                $str = $n[$i];
                $rankcols[] = (object)['width' => $width . '%', 'text' => $n[$i], 'pdfwidth' => $pdfwidth];
            } else {
                $str = $i + 1;
                $rankcols[] = (object)['width' => $width . '%', 'text' => $i + 1, 'pdfwidth' => $pdfwidth];
            }
        }
        $pagetags->averages->choicelabelrow = new \stdClass();
        $pagetags->averages->choicelabelrow->innertablewidth = $header2->pdfwidth;
        $pagetags->averages->choicelabelrow->column1 = (object)['width' => $header1->width, 'align' => $header1->align,
            'text' => '', 'pdfwidth' => $header1->pdfwidth];
        $pagetags->averages->choicelabelrow->column2 = (object)['width' => $header2->width, 'align' => $header2->align,
            'ranks' => $rankcols, 'pdfwidth' => $header2->pdfwidth];
        $pagetags->averages->choicelabelrow->column3 = (object)['width' => $header3->width, 'align' => $header3->align,
            'text' => '', 'pdfwidth' => $header3->pdfwidth];
        if ($isna) {
            $pagetags->averages->choicelabelrow->column4 = (object)['width' => $header4->width, 'align' => $header4->align,
                'text' => '', 'pdfwidth' => $header4->pdfwidth];
        }

        switch ($sort) {
            case 'ascending':
                uasort($this->counts, 'self::sortavgasc');
                break;
            case 'descending':
                uasort($this->counts, 'self::sortavgdesc');
                break;
        }
        reset ($this->counts);

        if (!empty($this->counts) && is_array($this->counts)) {
            $pagetags->averages->choiceaverages = [];
            foreach ($this->counts as $content => $contentobj) {
                // Eliminate potential named degrees on Likert scale.
                if (!preg_match("/^[0-9]{1,3}=/", $content)) {
                    if (isset($contentobj->avg)) {
                        $avg = $contentobj->avg;
                        // If named degrees were used, swap averages for display.
                        if (isset($contentobj->avgvalue)) {
                            $avg = $contentobj->avgvalue;
                            $avgvalue = $contentobj->avg;
                        } else {
                            $avgvalue = '';
                        }
                    } else {
                        $avg = '';
                    }
                    $nbna = $contentobj->nbna;

                    if ($avg) {
                        if (($j = $avg * $width) > 0) {
                            $marginposition = ($avg - 0.5 ) / ($this->question->length + $isrestricted);
                        }
                        if (!right_to_left()) {
                            $margin = 'margin-left:' . $marginposition * 100 . '%';
                            $marginpdf = $marginposition * $pagetags->averages->choicelabelrow->innertablewidth;
                        } else {
                            $margin = 'margin-right:' . $marginposition * 100 . '%';
                            $marginpdf = $pagetags->averages->choicelabelrow->innertablewidth -
                                ($marginposition * $pagetags->averages->choicelabelrow->innertablewidth);
                        }
                    } else {
                        $margin = '';
                    }

                    if ($osgood) {
                        // Ensure there are two bits of content.
                        list($content, $contentright) = array_merge(preg_split('/[|]/', $content), array(' '));
                    } else {
                        $contents = questionnaire_choice_values($content);
                        if ($contents->modname) {
                            $content = $contents->text;
                        }
                    }
                    if ($osgood) {
                        $choicecol1 = new \stdClass();
                        $choicecol1->width = $header1->width;
                        $choicecol1->pdfwidth = $header1->pdfwidth;
                        $choicecol1->align = $header1->align;
                        $choicecol1->text = '<div class="mdl-right">' .
                            format_text($content, FORMAT_HTML, ['noclean' => true]) . '</div>';
                        $choicecol2 = new \stdClass();
                        $choicecol2->width = $header2->width;
                        $choicecol2->pdfwidth = $header2->pdfwidth;
                        $choicecol2->align = $header2->align;
                        $choicecol2->imageurl = $imageurl;
                        $choicecol2->spacerimage = $spacerimage;
                        $choicecol2->margin = $margin;
                        $choicecol2->marginpdf = $marginpdf;
                        $choicecol3 = new \stdClass();
                        $choicecol3->width = $header3->width;
                        $choicecol3->pdfwidth = $header3->pdfwidth;
                        $choicecol3->align = $header3->align;
                        $choicecol3->text = '<div class="mdl-left">' .
                            format_text($contentright, FORMAT_HTML, ['noclean' => true]) . '</div>';
                        $pagetags->averages->choiceaverages[] = (object)['column1' => $choicecol1, 'column2' => $choicecol2,
                            'column3' => $choicecol3];
                        // JR JUNE 2012 do not display meaningless average rank values for Osgood.
                    } else if ($avg || ($nbna != 0)) {
                        $stravgval = '';
                        if ($avg) {
                            if ($stravgvalue) {
                                $stravgval = '('.sprintf('%.1f', $avgvalue).')';
                            }
                            $stravgval = sprintf('%.1f', $avg).'&nbsp;'.$stravgval;
                            if ($isna) {
                                $choicecol4 = new \stdClass();
                                $choicecol4->width = $header4->width;
                                $choicecol4->pdfwidth = $header4->pdfwidth;
                                $choicecol4->align = $header4->align;
                                $choicecol4->text = $nbna;
                            }
                        }
                        $choicecol1 = new \stdClass();
                        $choicecol1->width = $header1->width;
                        $choicecol1->pdfwidth = $header1->pdfwidth;
                        $choicecol1->align = $header1->align;
                        $choicecol1->text = format_text($content, FORMAT_HTML, ['noclean' => true]);
                        $choicecol2 = new \stdClass();
                        $choicecol2->width = $header2->width;
                        $choicecol2->pdfwidth = $header2->pdfwidth;
                        $choicecol2->align = $header2->align;
                        $choicecol2->imageurl = $imageurl;
                        $choicecol2->spacerimage = $spacerimage;
                        $choicecol2->margin = $margin;
                        $choicecol2->marginpdf = $marginpdf;
                        $choicecol3 = new \stdClass();
                        $choicecol3->width = $header3->width;
                        $choicecol3->pdfwidth = $header3->pdfwidth;
                        $choicecol3->align = $header3->align;
                        $choicecol3->text = $stravgval;
                        if ($avg) {
                            if (isset($choicecol4)) {
                                $pagetags->averages->choiceaverages[] = (object)['column1' => $choicecol1,
                                    'column2' => $choicecol2, 'column3' => $choicecol3, 'column4' => $choicecol4];
                            } else {
                                $pagetags->averages->choiceaverages[] = (object)['column1' => $choicecol1,
                                    'column2' => $choicecol2, 'column3' => $choicecol3];
                            }
                        } else {
                            $choicecol4 = new \stdClass();
                            $choicecol4->width = $header4->width;
                            $choicecol4->pdfwidth = $header4->pdfwidth;
                            $choicecol4->align = $header4->align;
                            $choicecol4->text = $nbna;
                            $pagetags->averages->choiceaverages[] = (object)['column1' => $choicecol1, 'column2' => $choicecol2,
                                'column3' => $choicecol3];
                        }
                    }
                } // End if named degrees.
            } // End foreach.
        } else {
            $nodata1 = new \stdClass();
            $nodata1->width = $header1->width;
            $nodata1->align = $header1->align;
            $nodata1->text = '';
            $nodata2 = new \stdClass();
            $nodata2->width = $header2->width;
            $nodata2->align = $header2->align;
            $nodata2->text = get_string('noresponsedata', 'mod_questionnaire');
            $nodata3 = new \stdClass();
            $nodata3->width = $header3->width;
            $nodata3->align = $header3->align;
            $nodata3->text = '';
            if (isset($header4)) {
                $nodata4 = new \stdClass();
                $nodata4->width = $header4->width;
                $nodata4->align = $header4->align;
                $nodata4->text = '';
                $pagetags->averages->nodata = [$nodata1, $nodata2, $nodata3, $nodata4];
            } else {
                $pagetags->averages->nodata = [$nodata1, $nodata2, $nodata3];
            }
        }
        return $pagetags;
    }

    /**
     * Return a structure for counts.
     * @param array $rids
     * @param array $rows
     * @param string $sort
     * @return \stdClass
     */
    private function mkrescount($rids, $rows, $sort) {
        // Display number of responses to Rate questions - see http://moodle.org/mod/forum/discuss.php?d=185106.
        global $DB;

        $nbresponses = count($rids);
        // Prepare data to be displayed.
        $isrestricted = ($this->question->length < count($this->question->choices)) && $this->question->precise == 2;

        $rsql = '';
        if (!empty($rids)) {
            list($rsql, $params) = $DB->get_in_or_equal($rids);
            $rsql = ' AND response_id ' . $rsql;
        }

        array_unshift($params, $this->question->id); // This is question_id.
        $sql = 'SELECT r.id, c.content, r.rankvalue, c.id AS choiceid ' .
            'FROM {questionnaire_quest_choice} c , ' .
            '{questionnaire_response_rank} r ' .
            'WHERE c.question_id = ?' .
            ' AND r.question_id = c.question_id' .
            ' AND r.choice_id = c.id ' .
            $rsql .
            ' ORDER BY choiceid, rankvalue ASC';
        $choices = $DB->get_records_sql($sql, $params);

        // Sort rows (results) by average value.
        if ($sort != 'default') {
            $sortarray = array();
            foreach ($rows as $row) {
                foreach ($row as $key => $value) {
                    if (!isset($sortarray[$key])) {
                        $sortarray[$key] = array();
                    }
                    $sortarray[$key][] = $value;
                }
            }
            $orderby = "average";
            switch ($sort) {
                case 'ascending':
                    array_multisort($sortarray[$orderby], SORT_ASC, $rows);
                    break;
                case 'descending':
                    array_multisort($sortarray[$orderby], SORT_DESC, $rows);
                    break;
            }
        }
        $nbranks = $this->question->length;
        $ranks = [];
        $rankvalue = [];
        if (!empty($this->question->nameddegrees)) {
            $rankvalue = array_flip(array_keys($this->question->nameddegrees));
        }
        foreach ($rows as $row) {
            $choiceid = $row->id;
            foreach ($choices as $choice) {
                if ($choice->choiceid == $choiceid) {
                    $n = 0;
                    for ($i = 1; $i <= $nbranks; $i++) {
                        if ((isset($rankvalue[$choice->rankvalue]) && ($rankvalue[$choice->rankvalue] == ($i - 1))) ||
                            (empty($rankvalue) && ($choice->rankvalue == $i))) {
                            $n++;
                            if (!isset($ranks[$choice->content][$i])) {
                                $ranks[$choice->content][$i] = 0;
                            }
                            $ranks[$choice->content][$i] += $n;
                        } else if (!isset($ranks[$choice->content][$i])) {
                            $ranks[$choice->content][$i] = 0;
                        }
                    }
                }
            }
        }

        // Psettings for display.
        $strtotal = '<strong>'.get_string('total', 'questionnaire').'</strong>';
        $isna = $this->question->precise == 1;
        $osgood = false;
        if ($this->question->precise == 3) { // Osgood's semantic differential.
            $osgood = true;
        }
        if ($this->question->precise == 1) {
            $na = get_string('notapplicable', 'questionnaire');
        } else {
            $na = '';
        }
        $nameddegrees = 0;
        $n = array();
        foreach ($this->question->nameddegrees as $degree) {
            $content = $degree;
            $n[$nameddegrees] = format_text($content, FORMAT_HTML, ['noclean' => true]);
            $nameddegrees++;
        }
        foreach ($this->question->choices as $choice) {
            $contents = questionnaire_choice_values($choice->content);
            if ($contents->modname) {
                $choice->content = $contents->text;
            }
        }

        $pagetags = new \stdClass();
        $pagetags->totals = new \stdClass();
        $pagetags->totals->headers = [];
        if ($osgood) {
            $align = 'right';
        } else {
            $align = 'left';
        }
        $pagetags->totals->headers[] = (object)['align' => $align,
            'text' => '<span class="smalltext">'.get_string('responses', 'questionnaire').'</span>'];

        // Display the column titles.
        for ($j = 0; $j < $this->question->length; $j++) {
            if (isset($n[$j])) {
                $str = $n[$j];
            } else {
                $str = $j + 1;
            }
            $pagetags->totals->headers[] = (object)['align' => 'center', 'text' => '<span class="smalltext">'.$str.'</span>'];
        }
        if ($osgood) {
            $pagetags->totals->headers[] = (object)['align' => 'left', 'text' => ''];
        }
        $pagetags->totals->headers[] = (object)['align' => 'center', 'text' => $strtotal];
        if ($isrestricted) {
            $pagetags->totals->headers[] = (object)['align' => 'center', 'text' => get_string('notapplicable', 'questionnaire')];
        }
        if ($na) {
            $pagetags->totals->headers[] = (object)['align' => 'center', 'text' => $na];
        }

        // Now display the responses.
        $pagetags->totals->choices = [];
        foreach ($ranks as $content => $rank) {
            $totalcols = [];
            // Eliminate potential named degrees on Likert scale.
            if (!preg_match("/^[0-9]{1,3}=/", $content)) {
                // First display the list of degrees (named or un-named)
                // number of NOT AVAILABLE responses for this possible answer.
                $nbna = $this->counts[$content]->nbna;
                // TOTAL number of responses for this possible answer.
                $total = $this->counts[$content]->num;
                $nbresp = '<strong>'.$total.'</strong>';
                if ($osgood) {
                    // Ensure there are two bits of content.
                    list($content, $contentright) = array_merge(preg_split('/[|]/', $content), array(' '));
                    $header = reset($pagetags->totals->headers);
                    $totalcols[] = (object)['align' => $header->align,
                        'text' => format_text($content, FORMAT_HTML, ['noclean' => true])];
                } else {
                    // Eliminate potentially short-named choices.
                    $contents = questionnaire_choice_values($content);
                    if ($contents->modname) {
                        $content = $contents->text;
                    }
                    $header = reset($pagetags->totals->headers);
                    $totalcols[] = (object)['align' => $header->align,
                        'text' => format_text($content, FORMAT_HTML, ['noclean' => true])];
                }
                // Display ranks/rates numbers.
                $maxrank = max($rank);
                for ($i = 1; $i <= $this->question->length; $i++) {
                    $percent = '';
                    if (isset($rank[$i])) {
                        $str = $rank[$i];
                        if ($total !== 0 && $str !== 0) {
                            $percent = ' (<span class="percent">'.number_format(($str * 100) / $total).'%</span>)';
                        }
                        // Emphasize responses with max rank value.
                        if ($str == $maxrank) {
                            $str = '<strong>'.$str.'</strong>';
                        }
                    } else {
                        $str = 0;
                    }
                    $header = next($pagetags->totals->headers);
                    $totalcols[] = (object)['align' => $header->align, 'text' => $str.$percent];
                }
                if ($osgood) {
                    $header = next($pagetags->totals->headers);
                    $totalcols[] = (object)['align' => $header->align,
                        'text' => format_text($contentright, FORMAT_HTML, ['noclean' => true])];
                }
                $header = next($pagetags->totals->headers);
                $totalcols[] = (object)['align' => $header->align, 'text' => $nbresp];
                if ($isrestricted) {
                    $header = next($pagetags->totals->headers);
                    $totalcols[] = (object)['align' => $header->align, 'text' => $nbresponses - $total];
                }
                if (!$osgood) {
                    if ($na) {
                        $header = next($pagetags->totals->headers);
                        $totalcols[] = (object)['align' => $header->align, 'text' => $nbna];
                    }
                }
            } // End named degrees.
            $pagetags->totals->choices[] = (object)['totalcols' => $totalcols];
        }
        return $pagetags;
    }

    /**
     * Sorting function for ascending.
     * @param \stdClass $a
     * @param \stdClass $b
     * @return int
     */
    private static function sortavgasc($a, $b) {
        if (isset($a->avg) && isset($b->avg)) {
            if ( $a->avg < $b->avg ) {
                return -1;
            } else if ($a->avg > $b->avg ) {
                return 1;
            } else {
                return 0;
            }
        }
    }

    /**
     * Sorting function for descending.
     * @param \stdClass $a
     * @param \stdClass $b
     * @return int
     */
    private static function sortavgdesc($a, $b) {
        if (isset($a->avg) && isset($b->avg)) {
            if ( $a->avg > $b->avg ) {
                return -1;
            } else if ($a->avg < $b->avg) {
                return 1;
            } else {
                return 0;
            }
        }
    }
}