Select Git revision
questiontestreport.php
-
Chris Sangwin authoredChris Sangwin authored
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();