Skip to content
Snippets Groups Projects
Select Git revision
  • fix-remote-url_v4.8.3
  • fix-remote-url_v4.8.x
  • fix-remote-url_v4.7.x
  • fix-remote-url_v4.6.0
  • fix-remote-urls
  • master default
6 results

questiontestreport.php

Blame
  • questiontestreport.php 20.05 KiB
    <?php
    // This file is part of Stack - http://stack.maths.ed.ac.uk/
    //
    // Stack 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.
    //
    // Stack 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 STACK.  If not, see <http://www.gnu.org/licenses/>.
    
    /**
     * This script lets the user see what attempts have been made at this question.
     *
     * The script loops over summarise_response data from the database, and does not
     * re-generate reports.  The script is designed to let a question author improve feedback
     * and assessment by looking at what students type, easily and without going through a quiz report.
     *
     * @copyright  2020 the University of Edinburgh
     * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
     */
    
    require_once(__DIR__.'/../../../config.php');
    require_once($CFG->libdir . '/questionlib.php');
    require_once(__DIR__ . '/vle_specific.php');
    
    // Get the parameters from the URL.
    $questionid = required_param('questionid', PARAM_INT);
    // Load the necessary data.
    $questiondata = question_bank::load_question_data($questionid);
    if (!$questiondata) {
        throw new stack_exception('questiondoesnotexist');
    }
    $question = question_bank::load_question($questionid);
    
    // Process any other URL parameters, and do require_login.
    list($context, $seed, $urlparams) = qtype_stack_setup_question_test_page($question);
    
    // Check permissions.
    question_require_capability_on($questiondata, 'view');
    $canedit = question_has_capability_on($questiondata, 'edit');
    
    // Initialise $PAGE.
    $PAGE->set_url('/question/type/stack/questiontestreport.php', $urlparams);
    $title = stack_string('basicquestionreport');
    $PAGE->set_title($title);
    $PAGE->set_heading($title);
    $PAGE->set_pagelayout('popup');
    
    $testquestionlink = new moodle_url('/question/type/stack/questiontestrun.php', $urlparams);
    
    require_login();
    
    // Start output.
    echo $OUTPUT->header();
    $renderer = $PAGE->get_renderer('qtype_stack');
    echo $OUTPUT->heading($question->name, 2);
    
    // Link back to question tests.
    $out = html_writer::link($testquestionlink, stack_string('runquestiontests'), array('target' => '_blank'));
    
    // If question has no random variants.
    if (empty($question->deployedseeds)) {
        if ($question->has_random_variants()) {
            $out .= ' ' . stack_string('questionnotdeployedyet');
        }
    }
    
    if (stack_determine_moodle_version() < 400) {
        $qurl = question_preview_url($questionid, null, null, null, null, $context);
    } else {
        $qurl = qbank_previewquestion\helper::question_preview_url($questionid, null, null, null, null, $context);
    }
    
    echo html_writer::tag('p', $out . ' ' .
        $OUTPUT->action_icon($qurl, new pix_icon('t/preview', get_string('preview'))));
    
    // Display a representation of the question, variables and PRTs for easy reference.
    echo $OUTPUT->heading(stack_string('questiontext'), 3);
    echo html_writer::tag('pre', $question->questiontext, array('class' => 'questiontext'));
    
    $vars = $question->questionvariables;
    if ($vars != '') {
        $vars = trim($vars) . "\n\n";
    }
    $inputdisplay = array($vars);
    foreach ($question->inputs as $inputname => $input) {
        $vars .= $inputname . ':' . $input->get_teacher_answer() . ";\n";
    }
    $maxima = html_writer::start_tag('div', array('class' => 'questionvariables'));
    $maxima .= html_writer::tag('pre', s(trim($vars)));
    $maxima .= html_writer::end_tag('div');
    echo $maxima;
    
    flush();
    
    // Later we only display inputs relevant to a particular PTR, so we sort out prt input requirements here.
    $inputsbyprt = $question->get_cached('required');
    
    $params = [$questionid];
    $query = "SELECT qa.*, qas_last.*
                  FROM {question_attempts} qa
                  LEFT JOIN {question_attempt_steps} qas_last ON qas_last.questionattemptid = qa.id
                  /* attach another copy of qas to those rows with the most recent timecreated,
                  using method from https://stackoverflow.com/a/28090544 */
                  LEFT JOIN {question_attempt_steps} qas_prev
                                ON qas_last.questionattemptid = qas_prev.questionattemptid
                                    AND (qas_last.sequencenumber < qas_prev.sequencenumber
                                        OR (qas_last.sequencenumber = qas_prev.sequencenumber
                                            AND qas_last.id < qas_prev.id))
                  LEFT JOIN {user} u ON qas_last.userid = u.id
              WHERE qas_prev.timecreated IS NULL";
    
    if (stack_determine_moodle_version() < 400) {
        $query .= " AND qa.questionid = ?";
    } else {
        // In moodle 4 we look at all attempts at all versions.
        // Otherwise an edit, regrade and re-analysis becomes impossible.
        $query .= " AND qa.questionid IN (
                        SELECT qv.questionid
                          FROM {question_versions} qv_original
                          JOIN {question_versions} qv ON
                                    qv.questionbankentryid = qv_original.questionbankentryid
                        WHERE qv_original.questionid = ?)";
    }
    $query .= " ORDER BY u.username, qas_last.timecreated";
    
    global $DB;
    
    $result = $DB->get_records_sql($query, $params);
    $summary = array();
    foreach ($result as $qattempt) {
        if (!array_key_exists($qattempt->variant, $summary)) {
            $summary[$qattempt->variant] = array();
        }
        $rsummary = trim($qattempt->responsesummary);
        if ($rsummary !== '') {
            if (array_key_exists($rsummary, $summary[$qattempt->variant])) {
                $summary[$qattempt->variant][$rsummary] += 1;
            } else {
                $summary[$qattempt->variant][$rsummary] = 1;
            }
        }
    }
    
    foreach ($summary as $vkey => $variant) {
        arsort($variant);
        $summary[$vkey] = $variant;
    }
    
    // Match up variants to answer notes.
    $questionnotes = array();
    $questionseeds = array();
    foreach (array_keys($summary) as $variant) {
        $questionnotes[$variant] = $variant;
    
        $question = question_bank::load_question($questionid);
        $question->start_attempt(new question_attempt_step(), $variant);
        $questionseeds[$variant] = $question->seed;
        $notesummary = $question->get_question_summary();
        // TODO check for duplicate notes.
        $questionnotes[$variant] = stack_ouput_castext($notesummary);
    }
    
    // Create blank arrays in which to store data.
    $qinputs = array_flip(array_keys($question->inputs));
    foreach ($qinputs as $key => $val) {
        $qinputs[$key] = array('score' => array(), 'valid' => array(), 'invalid' => array(), 'other' => array());
    }
    
    $inputreport = array();
    // The inputreportsummary is used to store inputs, regardless of variant.
    // Multi-part questions may have inputs which are not subject to randomisation.
    $inputreportsummary = $qinputs;
    $inputtotals = array();
    
    $qprts = array_flip(array_keys($question->prts));
    foreach ($qprts as $key => $notused) {
        $qprts[$key] = array();
    }
    $prtreport = array();
    $prtreportinputs = array();
    
    // Create a summary of the data without different variants.
    $prtreportsummary = array();
    
    foreach ($summary as $variant => $vdata) {
        $inputreport[$variant] = $qinputs;
        $prtreport[$variant] = $qprts;
        $prtreportinputs[$variant] = $qprts;
    
        foreach ($vdata as $attemptsummary => $num) {
            $inputvals = array();
            $rawdat = explode(';', $attemptsummary);
            foreach ($rawdat as $data) {
                $data = trim($data);
                foreach ($qinputs as $input => $notused) {
                    if (substr($data, 0, strlen($input . ':')) === $input . ':') {
                        // Tidy up inputs by (i) trimming status and whitespace, and (2) removing input name.
                        $datas = trim(substr($data, strlen($input . ':')));
                        $status = 'other';
                        if (strpos($datas, '[score]') !== false) {
                            $status = 'score';
                            $datas = trim(substr($datas, 0, -7));
                        } else if (strpos($datas, '[valid]') !== false) {
                            $status = 'valid';
                            $datas = trim(substr($datas, 0, -7));
                        } else if (strpos($datas, '[invalid]') !== false) {
                            $status = 'invalid';
                            $datas = trim(substr($datas, 0, -9));
                        }
                        // Reconstruct input string but whitespace is trimmed.
                        $inputvals[$input] = $input . ':' . $datas;
                        // Add data.
                        if (array_key_exists($datas, $inputreport[$variant][$input][$status])) {
                            $inputreport[$variant][$input][$status][$datas] += (int) $num;
                        } else {
                            $inputreport[$variant][$input][$status][$datas] = $num;
                        }
                        if (array_key_exists($datas, $inputreportsummary[$input][$status])) {
                            $inputreportsummary[$input][$status][$datas] += (int) $num;
                        } else {
                            $inputreportsummary[$input][$status][$datas] = $num;
                        }
                        // Count the total numbers in this array.
                        if (array_key_exists($input, $inputtotals)) {
                            $inputtotals[$input] += (int) $num;
                        } else {
                            $inputtotals[$input] = $num;
                        }
                    }
                }
                foreach ($qprts as $prt => $notused) {
                    // Only create an input summary of the inputs required for this PRT.
                    $inputsummary = '';
                    foreach ($inputsbyprt[$prt] as $input => $alsonotused) {
                        if (array_key_exists($input, $inputvals)) {
                            $inputsummary .= $inputvals[$input] . '; ';
                        }
                    }
                    if (substr($data, 0, strlen($prt . ':')) === $prt . ':') {
                        $datas = trim(substr($data, strlen($prt . ':')));
                        if (array_key_exists($datas, $prtreport[$variant][$prt])) {
                            $prtreport[$variant][$prt][$datas] += (int) $num;
                            if (array_key_exists($inputsummary, $prtreportinputs[$variant][$prt][$datas])) {
                                $prtreportinputs[$variant][$prt][$datas][$inputsummary] += (int) $num;
                            } else {
                                $prtreportinputs[$variant][$prt][$datas][$inputsummary] = (int) $num;
                            }
                        } else {
                            $prtreport[$variant][$prt][$datas] = $num;
                            $prtreportinputs[$variant][$prt][$datas] = array($inputsummary => (int) $num);
                        }
                        if (!array_key_exists($prt, $prtreportsummary)) {
                            $prtreportsummary[$prt] = array();
                        }
                        if (array_key_exists($datas, $prtreportsummary[$prt])) {
                            $prtreportsummary[$prt][$datas] += (int) $num;
                        } else {
                            $prtreportsummary[$prt][$datas] = $num;
                        }
                    }
                }
            }
        }
    }
    
    // Sort the values.
    foreach ($inputreport as $variant => $vdata) {
        foreach ($vdata as $input => $idata) {
            foreach ($idata as $key => $value) {
                arsort($value);
                $inputreport[$variant][$input][$key] = $value;
            }
        }
    }
    
    foreach ($inputreportsummary as $input => $idata) {
        foreach ($idata as $key => $value) {
            arsort($value);
            $inputreportsummary[$input][$key] = $value;
        }
    }
    
    foreach ($prtreport as $variant => $vdata) {
        foreach ($vdata as $prt => $tdata) {
            arsort($tdata);
            $prtreport[$variant][$prt] = $tdata;
        }
    }
    
    $notesummary = array();
    foreach ($prtreportsummary as $prt => $tdata) {
        ksort($tdata);
        $prtreportsummary[$prt] = $tdata;
        if (!array_key_exists($prt, $notesummary)) {
            $notesummary[$prt] = array();
        }
        foreach ($tdata as $rawnote => $num) {
            $notes = explode('|', $rawnote);
            foreach ($notes as $note) {
                $note = trim($note);
                if (array_key_exists($note, $notesummary[$prt])) {
                    $notesummary[$prt][$note] += (int) $num;
                } else {
                    $notesummary[$prt][$note] = $num;
                }
            }
        }
    
        foreach ($prtreportinputs[$variant][$prt] as $note => $ipts) {
            arsort($ipts);
            $prtreportinputs[$variant][$prt][$note] = $ipts;
        }
    }
    
    foreach ($notesummary as $prt => $tdata) {
        ksort($tdata);
        $notesummary[$prt] = $tdata;
    }
    
    // Frequency of answer notes, for each PRT, split by |, regardless of which variant was used.
    echo html_writer::tag('h3', stack_string('basicreportnotes'));
    
    $sumout = array();
    foreach ($prtreportsummary as $prt => $data) {
        $sumouti = '';
        $tot = 0;
        $pad = max($data);
        foreach ($data as $key => $val) {
            $tot += $val;
        }
        if ($data !== array()) {
            foreach ($data as $dat => $num) {
                $sumouti .= str_pad($num, strlen((string) $pad) + 1) . '(' .
                    str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) .
                    '%); ' . $dat . "\n";
            }
        }
        if (trim($sumouti) !== '') {
            $sumout[$prt] = '## ' . $prt . ' ('. $tot . ")\n" . $sumouti . "\n";;
        }
    }
    
    // Produce a text-based summary of a PRT.
    foreach ($question->prts as $prtname => $prt) {
        // Here we render each PRT as a separate single-row table.
        $tablerow = array($prtname);
    
        $graph = $prt->get_prt_graph();
        $tablerow[] = stack_abstract_graph_svg_renderer::render($graph, $prtname . 'graphsvg');
        $tablerow[] = stack_prt_graph_text_renderer::render($graph);
    
        $maxima = html_writer::tag('summary', $prtname) . html_writer::tag('pre', s($prt->get_maxima_representation()));
        $maxima = html_writer::tag('details', $maxima);
        $tablerow[] = html_writer::tag('div', $maxima, array('class' => 'questionvariables'));
    
        // Render the html as a single table.
        $html = '';
        foreach ($tablerow as $td) {
            $html .= html_writer::tag('td', $td);
        }
        $html = html_writer::tag('tr', $html);
        $html = html_writer::tag('table', $html);
        echo $html;
    
        if (array_key_exists($prtname, $sumout)) {
            echo html_writer::tag('pre', trim($sumout[$prtname]));
        }
    }
    
    $sumout = array();
    $prtlabels = array();
    foreach ($notesummary as $prt => $data) {
        $sumouti = '';
        if ($data !== array()) {
            foreach ($data as $dat => $num) {
                // Use the old $tot, to give meaningful percentages of which individual notes occur overall.
                $prtlabels[$prt][$dat] = $num;
                $sumouti .= str_pad($num, strlen((string) $pad) + 1) . '(' .
                    str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) . '%); '.
                    $dat . "\n";
            }
        }
        if (trim($sumouti) !== '') {
            $sumout[$prt] = '## ' . $prt . ' ('. $tot . ")\n" . $sumouti . "\n";;
        }
    }
    if (trim(implode($sumout)) !== '') {
        echo html_writer::tag('h3', stack_string('basicreportnotessplit'));
    }
    
    $tablerows = array();
    foreach ($question->prts as $prtname => $prt) {
        if (array_key_exists($prtname, $prtlabels)) {
            $tablerow = array($prtname);
    
            $graph = $prt->get_prt_graph();
            $tablerow[] = stack_abstract_graph_svg_renderer::render($graph, $prtname . 'graphsvg');
            $tablerow[] = stack_prt_graph_text_renderer::render($graph);
    
            $maxima = html_writer::tag('pre', s($sumout[$prtname]));
            $tablerow[] = html_writer::tag('div', $maxima, array('class' => 'questionvariables'));
    
            $tablerows[] = $tablerow;
        }
    }
    // Now create the HTML table.
    $html = '';
    foreach ($tablerows as $tablerow) {
        $rowhtml = '';
        foreach ($tablerow as $td) {
            $rowhtml .= html_writer::tag('td', $td);
        }
        $html .= html_writer::tag('tr', $rowhtml);
    }
    echo html_writer::tag('table', $html);
    
    // Raw inputs and PRT answer notes by variant.
    if (array_keys($summary) !== array()) {
        echo html_writer::tag('h3', stack_string('basicreportvariants'));
    }
    foreach (array_keys($summary) as $variant) {
        $sumout = '';
        foreach ($prtreport[$variant] as $prt => $idata) {
            $pad = 0;
            $tot = 0;
            foreach ($idata as $dat => $num) {
                $tot += $num;
            }
            if ($idata !== array()) {
                $sumout .= '## ' . $prt . ' ('. $tot . ")\n";
                $pad = max($idata);
            }
            foreach ($idata as $dat => $num) {
                $sumout .= str_pad($num, strlen((string) $pad) + 1) . '(' .
                    str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) .
                    '%); ' . $dat . "\n";
                foreach ($prtreportinputs[$variant][$prt][$dat] as $inputsummary => $inum) {
                    $sumout .= str_pad($inum, strlen((string) $pad) + 1) . '(' .
                        str_pad(number_format((float) 100 * $inum / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) .
                        '%); ' . $inputsummary . "\n";
                }
                $sumout .= "\n";
            }
            $sumout .= "\n";
        }
        if (trim($sumout) !== '') {
            echo html_writer::tag('h3', $questionseeds[$variant] . ': ' . $questionnotes[$variant]);
            echo html_writer::tag('pre', $sumout);
        }
    }
    
    
    foreach (array_keys($summary) as $variant) {
        $sumout = '';
        foreach ($inputreport[$variant] as $input => $idata) {
            $sumouti = '';
            $tot = 0;
            foreach ($idata as $key => $data) {
                foreach ($data as $dat => $num) {
                    $tot += $num;
                }
            }
            foreach ($idata as $key => $data) {
                if ($data !== array()) {
                    $sumouti .= '### ' . $key . "\n";
                    $pad = max($data);
                    foreach ($data as $dat => $num) {
                        $sumouti .= str_pad($num, strlen((string) $pad) + 1) . '(' .
                            str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) .
                            '%); ' . $dat . "\n";
                    }
                    $sumouti .= "\n";
                }
            }
            if (trim($sumouti) !== '') {
                $sumout .= '## ' . $input . ' ('. $tot . ")\n" . $sumouti;
            }
        }
        if (trim($sumout) !== '') {
            echo html_writer::tag('h3', $questionseeds[$variant] . ': ' . $questionnotes[$variant]);
            echo html_writer::tag('pre', $sumout);
        }
    }
    
    // Summary of just the inputs.
    $sumout = '';
    foreach ($inputreportsummary as $input => $idata) {
        $sumouti = '';
        $tot = 0;
        foreach ($idata as $key => $data) {
            foreach ($data as $dat => $num) {
                $tot += $num;
            }
        }
        foreach ($idata as $key => $data) {
            if ($data !== array()) {
                $sumouti .= '### ' . $key . "\n";
                $pad = max($data);
                foreach ($data as $dat => $num) {
                    $sumouti .= str_pad($num, strlen((string) $pad) + 1) . '(' .
                            str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) .
                            '%); ' . $dat . "\n";
                }
                $sumouti .= "\n";
            }
        }
        if (trim($sumouti) !== '') {
            $sumout .= '## ' . $input . ' ('. $tot . ")\n" . $sumouti;
        }
    }
    if (trim($sumout) !== '') {
        echo html_writer::tag('h3', stack_string('basicreportinputsummary'));
        echo html_writer::tag('pre', $sumout);
    }
    
    // Output the raw data.
    echo html_writer::tag('h3', stack_string('basicreportraw'));
    $sumout = '';
    foreach ($summary as $variant => $vdata) {
        if ($vdata !== array()) {
            $tot = 0;
            foreach ($vdata as $dat => $num) {
                $tot += $num;
            }
            $pad = max($vdata);
            $sumout .= "\n# " . $variant . ' ('. $tot . ")\n";
            foreach ($vdata as $dat => $num) {
                $sumout .= str_pad($num, strlen((string) $pad) + 1) . '(' .
                        str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) .
                        '%); ' . $dat . "\n";
            }
        }
    }
    echo html_writer::tag('pre', $sumout);
    
    // Finish output.
    echo $OUTPUT->footer();